From 4af6296587c787b5435823268b5591166e4ffd89 Mon Sep 17 00:00:00 2001 From: searce Date: Thu, 29 Jun 2017 15:48:31 +0530 Subject: [PATCH] travis corrected --- .coveragerc | 12 + .gitignore | 21 + .travis.yml | 29 + AUTHORING_GUIDE.md | 481 ++++++++++ CONTRIBUTING.md | 36 + ISSUE_TEMPLATE.md | 12 + LICENSE | 201 ++++ README.md | 17 + appengine/flexible/.gitignore | 54 ++ appengine/flexible/README.md | 68 ++ appengine/flexible/analytics/README.md | 20 + appengine/flexible/analytics/app.yaml | 11 + appengine/flexible/analytics/main.py | 74 ++ appengine/flexible/analytics/main_test.py | 47 + appengine/flexible/analytics/requirements.txt | 3 + appengine/flexible/cloudsql/README.md | 52 ++ appengine/flexible/cloudsql/app.yaml | 21 + appengine/flexible/cloudsql/create_tables.py | 25 + appengine/flexible/cloudsql/main.py | 97 ++ appengine/flexible/cloudsql/main_test.py | 26 + appengine/flexible/cloudsql/requirements.txt | 5 + appengine/flexible/datastore/README.md | 19 + appengine/flexible/datastore/app.yaml | 6 + appengine/flexible/datastore/main.py | 80 ++ appengine/flexible/datastore/main_test.py | 24 + appengine/flexible/datastore/requirements.txt | 3 + appengine/flexible/disk/app.yaml | 6 + appengine/flexible/disk/main.py | 74 ++ appengine/flexible/disk/main_test.py | 24 + appengine/flexible/disk/requirements.txt | 2 + appengine/flexible/django_cloudsql/README.md | 20 + appengine/flexible/django_cloudsql/app.yaml | 11 + appengine/flexible/django_cloudsql/manage.py | 24 + .../django_cloudsql/mysite/__init__.py | 0 .../django_cloudsql/mysite/settings.py | 125 +++ .../flexible/django_cloudsql/mysite/urls.py | 28 + .../flexible/django_cloudsql/mysite/wsgi.py | 22 + .../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 + .../flexible/django_cloudsql/polls/tests.py | 15 + .../flexible/django_cloudsql/polls/urls.py | 21 + .../flexible/django_cloudsql/polls/views.py | 19 + .../flexible/django_cloudsql/requirements.txt | 5 + .../flexible/extending_runtime/.dockerignore | 8 + .../flexible/extending_runtime/Dockerfile | 20 + appengine/flexible/extending_runtime/app.yaml | 2 + appengine/flexible/extending_runtime/main.py | 46 + .../flexible/extending_runtime/main_test.py | 31 + .../extending_runtime/requirements.txt | 2 + appengine/flexible/hello_world/app.yaml | 6 + appengine/flexible/hello_world/main.py | 43 + appengine/flexible/hello_world/main_test.py | 24 + .../flexible/hello_world/requirements.txt | 2 + .../flexible/hello_world_django/.gitignore | 1 + .../flexible/hello_world_django/README.md | 58 ++ .../flexible/hello_world_django/app.yaml | 6 + .../hello_world_django/helloworld/__init__.py | 0 .../hello_world_django/helloworld/views.py | 22 + .../flexible/hello_world_django/manage.py | 10 + .../project_name/__init__.py | 0 .../project_name/settings.py | 117 +++ .../hello_world_django/project_name/urls.py | 39 + .../hello_world_django/project_name/wsgi.py | 16 + .../hello_world_django/requirements.txt | 2 + appengine/flexible/mailgun/README.md | 24 + appengine/flexible/mailgun/app.yaml | 12 + .../flexible/mailgun/example-attachment.txt | 1 + appengine/flexible/mailgun/main.py | 95 ++ appengine/flexible/mailgun/main_test.py | 80 ++ appengine/flexible/mailgun/requirements.txt | 3 + .../flexible/mailgun/templates/index.html | 30 + appengine/flexible/mailjet/README.md | 23 + appengine/flexible/mailjet/app.yaml | 13 + appengine/flexible/mailjet/main.py | 78 ++ appengine/flexible/mailjet/main_test.py | 53 ++ appengine/flexible/mailjet/requirements.txt | 4 + .../flexible/mailjet/templates/index.html | 29 + appengine/flexible/memcache/README.md | 44 + appengine/flexible/memcache/app.yaml | 16 + appengine/flexible/memcache/main.py | 62 ++ appengine/flexible/memcache/main_test.py | 41 + appengine/flexible/memcache/requirements.txt | 3 + appengine/flexible/metadata/app.yaml | 6 + appengine/flexible/metadata/main.py | 70 ++ appengine/flexible/metadata/requirements.txt | 3 + .../flexible/multiple_services/README.md | 62 ++ .../gateway-service/app.yaml | 10 + .../multiple_services/gateway-service/main.py | 58 ++ .../gateway-service/requirements.txt | 3 + .../gateway-service/services_config.py | 57 ++ .../multiple_services/static-service/app.yaml | 10 + .../multiple_services/static-service/main.py | 46 + .../static-service/requirements.txt | 3 + .../static-service/static/index.html | 32 + .../static-service/static/index.js | 36 + .../static-service/static/style.css | 3 + appengine/flexible/numpy/app.yaml | 6 + appengine/flexible/numpy/main.py | 50 + appengine/flexible/numpy/main_test.py | 24 + appengine/flexible/numpy/requirements.txt | 3 + appengine/flexible/pubsub/README.md | 71 ++ appengine/flexible/pubsub/app.yaml | 14 + appengine/flexible/pubsub/main.py | 85 ++ appengine/flexible/pubsub/main_test.py | 69 ++ appengine/flexible/pubsub/requirements.txt | 3 + appengine/flexible/pubsub/sample_message.json | 5 + .../flexible/pubsub/templates/index.html | 38 + appengine/flexible/redis/app.yaml | 13 + appengine/flexible/redis/main.py | 54 ++ appengine/flexible/redis/main_test.py | 35 + appengine/flexible/redis/requirements.txt | 3 + appengine/flexible/scipy/.gitignore | 1 + appengine/flexible/scipy/README.md | 4 + appengine/flexible/scipy/app.yaml | 6 + .../flexible/scipy/assets/google_logo.jpg | Bin 0 -> 21568 bytes .../fixtures/assets/resized_google_logo.jpg | Bin 0 -> 8639 bytes appengine/flexible/scipy/main.py | 52 ++ appengine/flexible/scipy/main_test.py | 35 + appengine/flexible/scipy/requirements.txt | 5 + appengine/flexible/sendgrid/README.md | 24 + appengine/flexible/sendgrid/app.yaml | 12 + appengine/flexible/sendgrid/main.py | 74 ++ appengine/flexible/sendgrid/main_test.py | 49 + appengine/flexible/sendgrid/requirements.txt | 3 + .../flexible/sendgrid/templates/index.html | 29 + appengine/flexible/static_files/README.md | 7 + appengine/flexible/static_files/app.yaml | 6 + appengine/flexible/static_files/main.py | 42 + appengine/flexible/static_files/main_test.py | 26 + .../flexible/static_files/requirements.txt | 2 + .../flexible/static_files/static/main.css | 21 + .../static_files/templates/index.html | 29 + appengine/flexible/storage/README.md | 32 + appengine/flexible/storage/app.yaml | 11 + appengine/flexible/storage/main.py | 84 ++ appengine/flexible/storage/main_test.py | 50 + appengine/flexible/storage/requirements.txt | 3 + appengine/flexible/twilio/README.md | 29 + appengine/flexible/twilio/app.yaml | 13 + appengine/flexible/twilio/main.py | 90 ++ appengine/flexible/twilio/main_test.py | 67 ++ appengine/flexible/twilio/requirements.txt | 3 + appengine/standard/README.md | 42 + appengine/standard/analytics/app.yaml | 12 + .../standard/analytics/appengine_config.py | 18 + appengine/standard/analytics/main.py | 77 ++ appengine/standard/analytics/main_test.py | 47 + appengine/standard/analytics/requirements.txt | 3 + appengine/standard/angular/README.md | 5 + appengine/standard/angular/app.yaml | 15 + appengine/standard/angular/app/css/app.css | 5 + appengine/standard/angular/app/index.html | 14 + appengine/standard/angular/app/js/app.js | 123 +++ .../standard/angular/app/partials/insert.html | 12 + .../standard/angular/app/partials/main.html | 7 + .../standard/angular/app/partials/update.html | 16 + appengine/standard/angular/main.py | 75 ++ appengine/standard/angular/model.py | 41 + appengine/standard/angular/scripts/deploy.sh | 72 ++ appengine/standard/angular/scripts/run.sh | 9 + .../standard/app_identity/asserting/app.yaml | 7 + .../standard/app_identity/asserting/main.py | 62 ++ .../app_identity/asserting/main_test.py | 32 + .../standard/app_identity/incoming/app.yaml | 7 + .../standard/app_identity/incoming/main.py | 46 + .../app_identity/incoming/main_test.py | 28 + .../standard/app_identity/signing/app.yaml | 11 + .../standard/app_identity/signing/main.py | 84 ++ .../app_identity/signing/main_test.py | 24 + appengine/standard/appstats/app.yaml | 10 + .../standard/appstats/appengine_config.py | 20 + appengine/standard/appstats/main.py | 37 + appengine/standard/appstats/main_test.py | 22 + appengine/standard/background/README.md | 9 + appengine/standard/background/app.yaml | 10 + appengine/standard/background/main.py | 86 ++ appengine/standard/background/main_test.py | 57 ++ appengine/standard/bigquery/.gitignore | 1 + appengine/standard/bigquery/README.md | 20 + appengine/standard/bigquery/app.yaml | 9 + .../standard/bigquery/appengine_config.py | 4 + .../standard/bigquery/client_secrets.json | 3 + appengine/standard/bigquery/main.py | 68 ++ appengine/standard/bigquery/main_test.py | 69 ++ appengine/standard/bigquery/requirements.txt | 1 + .../bigquery/resources/datasets-list.json | 14 + appengine/standard/blobstore/api/README.md | 11 + appengine/standard/blobstore/api/app.yaml | 8 + appengine/standard/blobstore/api/main.py | 85 ++ appengine/standard/blobstore/api/main_test.py | 26 + .../standard/blobstore/blobreader/app.yaml | 8 + .../blobstore/blobreader/appengine_config.py | 18 + .../standard/blobstore/blobreader/main.py | 69 ++ .../blobstore/blobreader/main_test.py | 25 + .../blobstore/blobreader/requirements.txt | 1 + appengine/standard/blobstore/gcs/app.yaml | 8 + .../blobstore/gcs/appengine_config.py | 18 + appengine/standard/blobstore/gcs/main.py | 76 ++ appengine/standard/blobstore/gcs/main_test.py | 34 + .../standard/blobstore/gcs/requirements.txt | 1 + appengine/standard/channel/README.md | 4 + appengine/standard/channel/app.yaml | 15 + appengine/standard/channel/chatactoe.py | 204 +++++ appengine/standard/channel/index.html | 259 ++++++ appengine/standard/cloudsql/README.md | 11 + appengine/standard/cloudsql/app.yaml | 18 + appengine/standard/cloudsql/main.py | 79 ++ appengine/standard/cloudsql/main_test.py | 40 + appengine/standard/conftest.py | 40 + appengine/standard/django/README.md | 10 + appengine/standard/django/app.yaml | 31 + appengine/standard/django/appengine_config.py | 19 + appengine/standard/django/manage.py | 24 + appengine/standard/django/mysite/__init__.py | 0 appengine/standard/django/mysite/settings.py | 145 +++ appengine/standard/django/mysite/urls.py | 23 + appengine/standard/django/mysite/wsgi.py | 29 + appengine/standard/django/polls/__init__.py | 0 appengine/standard/django/polls/admin.py | 19 + appengine/standard/django/polls/models.py | 26 + appengine/standard/django/polls/tests.py | 20 + appengine/standard/django/polls/views.py | 19 + .../standard/django/requirements-vendor.txt | 1 + appengine/standard/django/requirements.txt | 2 + .../endpoints-frameworks-v2/echo/README.md | 33 + .../endpoints-frameworks-v2/echo/app.yaml | 32 + .../echo/appengine_config.py | 4 + .../endpoints-frameworks-v2/echo/main.py | 104 +++ .../endpoints-frameworks-v2/echo/main_test.py | 41 + .../echo/requirements.txt | 2 + .../quickstart/app.yaml | 28 + .../quickstart/appengine_config.py | 4 + .../quickstart/main.py | 149 +++ .../quickstart/main_test.py | 54 ++ appengine/standard/endpoints/backend/app.yaml | 16 + appengine/standard/endpoints/backend/main.py | 149 +++ .../standard/endpoints/backend/main_test.py | 54 ++ .../standard/endpoints/multiapi/app.yaml | 16 + appengine/standard/endpoints/multiapi/main.py | 55 ++ .../standard/endpoints/multiapi/main_test.py | 27 + .../standard/firebase/firenotes/README.md | 59 ++ .../firebase/firenotes/backend/.gitignore | 1 + .../firebase/firenotes/backend/app.yaml | 12 + .../firenotes/backend/appengine_config.py | 18 + .../firebase/firenotes/backend/index.yaml | 22 + .../firebase/firenotes/backend/main.py | 133 +++ .../firebase/firenotes/backend/main_test.py | 96 ++ .../firenotes/backend/requirements.txt | 6 + .../firebase/firenotes/frontend/app.yaml | 15 + .../firebase/firenotes/frontend/index.html | 44 + .../firebase/firenotes/frontend/main.js | 156 ++++ .../firebase/firenotes/frontend/style.css | 44 + .../standard/firebase/firetactoe/README.md | 44 + .../standard/firebase/firetactoe/app.yaml | 11 + .../firebase/firetactoe/appengine_config.py | 10 + .../firebase/firetactoe/firetactoe.py | 274 ++++++ .../firebase/firetactoe/firetactoe_test.py | 161 ++++ .../firebase/firetactoe/requirements.txt | 5 + .../standard/firebase/firetactoe/rest_api.py | 114 +++ .../firebase/firetactoe/static/main.css | 82 ++ .../firebase/firetactoe/static/main.js | 178 ++++ .../templates/_firebase_config.html | 3 + .../firetactoe/templates/fire_index.html | 43 + .../standard/flask/hello_world/.gitignore | 1 + .../standard/flask/hello_world/README.md | 11 + appengine/standard/flask/hello_world/app.yaml | 7 + .../flask/hello_world/appengine_config.py | 18 + appengine/standard/flask/hello_world/main.py | 34 + .../standard/flask/hello_world/main_test.py | 27 + .../flask/hello_world/requirements.txt | 1 + appengine/standard/flask/tutorial/.gitignore | 1 + appengine/standard/flask/tutorial/README.md | 11 + appengine/standard/flask/tutorial/app.yaml | 11 + .../flask/tutorial/appengine_config.py | 20 + appengine/standard/flask/tutorial/main.py | 58 ++ .../standard/flask/tutorial/main_test.py | 38 + .../standard/flask/tutorial/requirements.txt | 1 + .../standard/flask/tutorial/static/style.css | 3 + .../flask/tutorial/templates/form.html | 26 + .../tutorial/templates/submitted_form.html | 23 + appengine/standard/hello_world/app.yaml | 7 + appengine/standard/hello_world/main.py | 26 + appengine/standard/hello_world/main_test.py | 26 + appengine/standard/i18n/README.md | 128 +++ appengine/standard/i18n/app.yaml | 24 + appengine/standard/i18n/appengine_config.py | 35 + appengine/standard/i18n/i18n_utils.py | 207 +++++ appengine/standard/i18n/js.mapping | 1 + .../i18n/locales/en/LC_MESSAGES/jsmessages.mo | Bin 0 -> 526 bytes .../i18n/locales/en/LC_MESSAGES/jsmessages.po | 18 + .../i18n/locales/en/LC_MESSAGES/messages.mo | Bin 0 -> 590 bytes .../i18n/locales/en/LC_MESSAGES/messages.po | 23 + .../i18n/locales/ja/LC_MESSAGES/jsmessages.mo | Bin 0 -> 538 bytes .../i18n/locales/ja/LC_MESSAGES/jsmessages.po | 18 + .../i18n/locales/ja/LC_MESSAGES/messages.mo | Bin 0 -> 650 bytes .../i18n/locales/ja/LC_MESSAGES/messages.po | 23 + .../standard/i18n/locales/jsmessages.pot | 18 + appengine/standard/i18n/locales/messages.pot | 22 + .../i18n/locales/pl/LC_MESSAGES/jsmessages.mo | Bin 0 -> 584 bytes .../i18n/locales/pl/LC_MESSAGES/jsmessages.po | 19 + appengine/standard/i18n/main.mapping | 8 + appengine/standard/i18n/main.py | 49 + appengine/standard/i18n/static/js/main.js | 9 + .../standard/i18n/templates/i18n_js.jinja2 | 38 + .../standard/i18n/templates/index.jinja2 | 13 + .../i18n/templates/javascript_tag.jinja2 | 3 + .../i18n/templates/null_i18n_js.jinja2 | 15 + appengine/standard/images/api/README.md | 13 + appengine/standard/images/api/app.yaml | 8 + appengine/standard/images/api/blobstore.py | 52 ++ .../standard/images/api/blobstore_test.py | 46 + appengine/standard/images/api/favicon.ico | Bin 0 -> 8348 bytes appengine/standard/images/api/main.py | 56 ++ appengine/standard/images/api/main_test.py | 50 + appengine/standard/images/guestbook/README.md | 13 + appengine/standard/images/guestbook/app.yaml | 8 + .../standard/images/guestbook/favicon.ico | Bin 0 -> 8348 bytes .../standard/images/guestbook/index.yaml | 17 + appengine/standard/images/guestbook/main.py | 138 +++ .../standard/images/guestbook/main_test.py | 78 ++ appengine/standard/localtesting/README.md | 10 + .../standard/localtesting/datastore_test.py | 165 ++++ .../standard/localtesting/env_vars_test.py | 41 + appengine/standard/localtesting/login_test.py | 54 ++ appengine/standard/localtesting/mail_test.py | 45 + appengine/standard/localtesting/queue.yaml | 7 + .../localtesting/resources/queue.yaml | 7 + appengine/standard/localtesting/runner.py | 102 +++ .../standard/localtesting/task_queue_test.py | 90 ++ .../standard/logging/reading_logs/app.yaml | 7 + .../standard/logging/reading_logs/main.py | 103 +++ .../logging/reading_logs/main_test.py | 25 + .../standard/logging/writing_logs/app.yaml | 7 + .../standard/logging/writing_logs/main.py | 47 + .../logging/writing_logs/main_test.py | 24 + appengine/standard/mail/README.md | 17 + appengine/standard/mail/app.yaml | 47 + appengine/standard/mail/attachment.py | 50 + appengine/standard/mail/attachment_test.py | 27 + .../standard/mail/handle_bounced_email.py | 30 + .../mail/handle_bounced_email_test.py | 24 + .../standard/mail/handle_incoming_email.py | 42 + .../mail/handle_incoming_email_test.py | 28 + appengine/standard/mail/header.py | 55 ++ appengine/standard/mail/header_test.py | 27 + appengine/standard/mail/index.html | 29 + appengine/standard/mail/send_mail.py | 48 + appengine/standard/mail/send_mail_test.py | 26 + appengine/standard/mail/send_message.py | 51 ++ appengine/standard/mail/send_message_test.py | 26 + appengine/standard/mail/user_signup.py | 106 +++ appengine/standard/mail/user_signup_test.py | 39 + appengine/standard/mailgun/.gitignore | 1 + appengine/standard/mailgun/README.md | 12 + appengine/standard/mailgun/app.yaml | 7 + .../standard/mailgun/appengine_config.py | 18 + appengine/standard/mailgun/main.py | 111 +++ appengine/standard/mailgun/main_test.py | 67 ++ appengine/standard/mailgun/requirements.txt | 1 + appengine/standard/mailjet/.gitignore | 1 + appengine/standard/mailjet/README.md | 11 + appengine/standard/mailjet/app.yaml | 14 + .../standard/mailjet/appengine_config.py | 18 + appengine/standard/mailjet/main.py | 77 ++ appengine/standard/mailjet/main_test.py | 53 ++ appengine/standard/mailjet/requirements.txt | 4 + .../standard/mailjet/templates/index.html | 29 + .../memcache/best_practices/README.md | 5 + .../memcache/best_practices/batch/app.yaml | 7 + .../memcache/best_practices/batch/batch.py | 36 + .../best_practices/batch/batch_test.py | 28 + .../memcache/best_practices/failure/app.yaml | 7 + .../best_practices/failure/failure.py | 73 ++ .../best_practices/failure/failure_test.py | 35 + .../best_practices/migration_step1/app.yaml | 7 + .../migration_step1/migration1.py | 49 + .../migration_step1/migration1_test.py | 22 + .../best_practices/migration_step2/app.yaml | 7 + .../migration_step2/migration2.py | 50 + .../migration_step2/migration2_test.py | 22 + .../memcache/best_practices/sharing/app.yaml | 7 + .../best_practices/sharing/sharing.py | 38 + .../best_practices/sharing/sharing_test.py | 23 + .../standard/memcache/guestbook/README.md | 13 + .../standard/memcache/guestbook/app.yaml | 21 + .../standard/memcache/guestbook/favicon.ico | Bin 0 -> 8348 bytes .../standard/memcache/guestbook/index.yaml | 17 + appengine/standard/memcache/guestbook/main.py | 151 ++++ .../standard/memcache/guestbook/main_test.py | 23 + .../standard/memcache/snippets/snippets.py | 54 ++ .../memcache/snippets/snippets_test.py | 50 + appengine/standard/modules/README.md | 5 + appengine/standard/modules/app.yaml | 7 + appengine/standard/modules/backend.py | 29 + appengine/standard/modules/backend.yaml | 8 + appengine/standard/modules/backend_test.py | 29 + appengine/standard/modules/main.py | 53 ++ appengine/standard/modules/main_test.py | 46 + appengine/standard/multitenancy/README.md | 12 + appengine/standard/multitenancy/app.yaml | 13 + appengine/standard/multitenancy/datastore.py | 70 ++ .../standard/multitenancy/datastore_test.py | 35 + appengine/standard/multitenancy/memcache.py | 53 ++ .../standard/multitenancy/memcache_test.py | 35 + appengine/standard/multitenancy/taskqueue.py | 91 ++ .../standard/multitenancy/taskqueue_test.py | 41 + appengine/standard/ndb/async/README.md | 11 + appengine/standard/ndb/async/app_async.py | 36 + .../standard/ndb/async/app_async_test.py | 36 + appengine/standard/ndb/async/app_sync.py | 35 + appengine/standard/ndb/async/app_sync_test.py | 36 + .../standard/ndb/async/app_toplevel/README.md | 2 + .../ndb/async/app_toplevel/app_toplevel.py | 39 + .../async/app_toplevel/app_toplevel_test.py | 36 + .../ndb/async/app_toplevel/index.html | 0 appengine/standard/ndb/async/guestbook.py | 101 +++ .../standard/ndb/async/guestbook_test.py | 74 ++ appengine/standard/ndb/async/shopping_cart.py | 137 +++ .../standard/ndb/async/shopping_cart_test.py | 144 +++ appengine/standard/ndb/cache/README.md | 11 + appengine/standard/ndb/cache/snippets.py | 45 + appengine/standard/ndb/cache/snippets_test.py | 60 ++ appengine/standard/ndb/entities/README.md | 15 + appengine/standard/ndb/entities/snippets.py | 294 ++++++ .../standard/ndb/entities/snippets_test.py | 228 +++++ appengine/standard/ndb/modeling/README.md | 5 + .../ndb/modeling/contact_with_group_models.py | 84 ++ .../contact_with_group_models_test.py | 46 + .../ndb/modeling/keyproperty_models.py | 64 ++ .../ndb/modeling/keyproperty_models_test.py | 45 + .../standard/ndb/modeling/naive_models.py | 47 + .../ndb/modeling/naive_models_test.py | 25 + .../ndb/modeling/parent_child_models.py | 63 ++ .../ndb/modeling/parent_child_models_test.py | 82 ++ .../ndb/modeling/relation_model_models.py | 112 +++ .../modeling/relation_model_models_test.py | 44 + .../modeling/structured_property_models.py | 51 ++ .../structured_property_models_test.py | 58 ++ appengine/standard/ndb/overview/README.md | 12 + appengine/standard/ndb/overview/app.yaml | 22 + appengine/standard/ndb/overview/favicon.ico | Bin 0 -> 8348 bytes appengine/standard/ndb/overview/index.yaml | 17 + appengine/standard/ndb/overview/main.py | 104 +++ appengine/standard/ndb/overview/main_test.py | 23 + .../standard/ndb/projection_queries/README.md | 11 + .../ndb/projection_queries/snippets.py | 61 ++ .../ndb/projection_queries/snippets_test.py | 46 + appengine/standard/ndb/properties/README.md | 11 + appengine/standard/ndb/properties/snippets.py | 150 +++ .../standard/ndb/properties/snippets_test.py | 106 +++ .../ndb/property_subclasses/README.md | 11 + .../ndb/property_subclasses/my_models.py | 106 +++ .../ndb/property_subclasses/snippets.py | 57 ++ .../ndb/property_subclasses/snippets_test.py | 98 ++ appengine/standard/ndb/queries/README.md | 11 + appengine/standard/ndb/queries/guestbook.py | 75 ++ .../standard/ndb/queries/guestbook_test.py | 67 ++ appengine/standard/ndb/queries/snippets.py | 236 +++++ .../standard/ndb/queries/snippets_models.py | 65 ++ .../standard/ndb/queries/snippets_test.py | 388 ++++++++ appengine/standard/ndb/schema_update/app.yaml | 17 + appengine/standard/ndb/schema_update/main.py | 139 +++ .../standard/ndb/schema_update/main_test.py | 62 ++ .../standard/ndb/schema_update/models_v1.py | 20 + .../standard/ndb/schema_update/models_v2.py | 23 + .../ndb/schema_update/templates/index.html | 47 + appengine/standard/ndb/transactions/README.md | 14 + appengine/standard/ndb/transactions/app.yaml | 18 + .../ndb/transactions/appengine_config.py | 11 + .../standard/ndb/transactions/favicon.ico | Bin 0 -> 8348 bytes appengine/standard/ndb/transactions/main.py | 193 ++++ .../standard/ndb/transactions/main_test.py | 50 + .../ndb/transactions/requirements.txt | 1 + appengine/standard/remote_api/app.yaml | 6 + appengine/standard/remote_api/client.py | 54 ++ appengine/standard/requests/README.md | 10 + appengine/standard/requests/app.yaml | 7 + appengine/standard/requests/main.py | 66 ++ appengine/standard/requests/main_test.py | 45 + appengine/standard/resources/app.yaml | 11 + .../standard/search/snippets/snippets.py | 287 ++++++ .../standard/search/snippets/snippets_test.py | 140 +++ appengine/standard/sendgrid/README.md | 13 + appengine/standard/sendgrid/app.yaml | 7 + .../standard/sendgrid/appengine_config.py | 18 + appengine/standard/sendgrid/main.py | 72 ++ appengine/standard/sendgrid/main_test.py | 46 + appengine/standard/sendgrid/requirements.txt | 1 + appengine/standard/storage/.gitignore | 1 + .../standard/storage/api-client/README.md | 15 + .../standard/storage/api-client/app.yaml | 7 + .../storage/api-client/appengine_config.py | 4 + appengine/standard/storage/api-client/main.py | 71 ++ .../standard/storage/api-client/main_test.py | 34 + .../storage/api-client/requirements.txt | 1 + .../storage/appengine-client/__init__.py | 0 .../storage/appengine-client/app.yaml | 12 + .../appengine-client/appengine_config.py | 4 + .../standard/storage/appengine-client/main.py | 167 ++++ .../storage/appengine-client/main_test.py | 31 + .../storage/appengine-client/requirements.txt | 1 + .../standard/taskqueue/counter/README.md | 18 + appengine/standard/taskqueue/counter/app.yaml | 8 + .../standard/taskqueue/counter/application.py | 82 ++ .../taskqueue/counter/application_test.py | 32 + .../standard/taskqueue/counter/queue.yaml | 4 + .../standard/taskqueue/counter/worker.py | 46 + .../standard/taskqueue/counter/worker.yaml | 9 + .../standard/taskqueue/pull-counter/README.md | 8 + .../standard/taskqueue/pull-counter/app.yaml | 11 + .../taskqueue/pull-counter/counter.html | 15 + .../standard/taskqueue/pull-counter/main.py | 91 ++ .../pull-counter/pullcounter_test.py | 43 + .../taskqueue/pull-counter/queue.yaml | 3 + .../taskqueue/pull-counter/worker.yaml | 15 + appengine/standard/urlfetch/README.md | 7 + appengine/standard/urlfetch/async/app.yaml | 7 + appengine/standard/urlfetch/async/rpc.py | 83 ++ appengine/standard/urlfetch/async/rpc_test.py | 78 ++ .../standard/urlfetch/requests/.gitignore | 1 + appengine/standard/urlfetch/requests/app.yaml | 7 + .../urlfetch/requests/appengine_config.py | 18 + appengine/standard/urlfetch/requests/main.py | 49 + .../standard/urlfetch/requests/main_test.py | 24 + .../urlfetch/requests/requirements.txt | 3 + appengine/standard/urlfetch/snippets/app.yaml | 7 + appengine/standard/urlfetch/snippets/main.py | 100 ++ .../standard/urlfetch/snippets/main_test.py | 43 + appengine/standard/users/app.yaml | 7 + appengine/standard/users/main.py | 60 ++ appengine/standard/users/main_test.py | 44 + appengine/standard/xmpp/README.md | 10 + appengine/standard/xmpp/app.yaml | 15 + appengine/standard/xmpp/xmpp.py | 104 +++ appengine/standard/xmpp/xmpp_test.py | 66 ++ auth/api-client/requirements.txt | 3 + auth/api-client/snippets.py | 108 +++ auth/api-client/snippets_test.py | 52 ++ auth/cloud-client/requirements.txt | 1 + auth/cloud-client/snippets.py | 83 ++ auth/cloud-client/snippets_test.py | 43 + auth/http-client/requirements.txt | 2 + auth/http-client/snippets.py | 79 ++ auth/http-client/snippets_test.py | 33 + bigquery/api/README.rst | 395 ++++++++ bigquery/api/README.rst.in | 46 + bigquery/api/async_query.py | 148 +++ bigquery/api/async_query_test.py | 55 ++ bigquery/api/export_data_to_cloud_storage.py | 170 ++++ .../api/export_data_to_cloud_storage_test.py | 62 ++ bigquery/api/getting_started.py | 75 ++ bigquery/api/getting_started_test.py | 28 + bigquery/api/installed_app.py | 84 ++ bigquery/api/installed_app_test.py | 50 + bigquery/api/list_datasets_projects.py | 77 ++ bigquery/api/list_datasets_projects_test.py | 30 + bigquery/api/load_data_by_post.py | 134 +++ bigquery/api/load_data_by_post_test.py | 62 ++ bigquery/api/load_data_from_csv.py | 164 ++++ bigquery/api/load_data_from_csv_test.py | 40 + bigquery/api/requirements.txt | 1 + bigquery/api/resources/data.csv | 1 + bigquery/api/resources/data.json | 1 + bigquery/api/resources/schema.json | 1 + bigquery/api/resources/streamrows.json | 7 + bigquery/api/streaming.py | 102 +++ bigquery/api/streaming_test.py | 40 + bigquery/api/sync_query.py | 112 +++ bigquery/api/sync_query_test.py | 53 ++ bigquery/cloud-client/README.rst | 337 +++++++ bigquery/cloud-client/README.rst.in | 43 + bigquery/cloud-client/async_query.py | 64 ++ bigquery/cloud-client/async_query_test.py | 27 + bigquery/cloud-client/export_data_to_gcs.py | 77 ++ .../cloud-client/export_data_to_gcs_test.py | 31 + bigquery/cloud-client/load_data_from_file.py | 78 ++ .../cloud-client/load_data_from_file_test.py | 33 + bigquery/cloud-client/load_data_from_gcs.py | 77 ++ .../cloud-client/load_data_from_gcs_test.py | 33 + bigquery/cloud-client/query_params.py | 233 +++++ bigquery/cloud-client/query_params_test.py | 52 ++ bigquery/cloud-client/quickstart.py | 40 + bigquery/cloud-client/quickstart_test.py | 45 + bigquery/cloud-client/requirements.txt | 3 + bigquery/cloud-client/resources/data.csv | 1 + bigquery/cloud-client/resources/data.json | 1 + bigquery/cloud-client/resources/schema.json | 1 + bigquery/cloud-client/simple_app.py | 50 + bigquery/cloud-client/simple_app_test.py | 21 + bigquery/cloud-client/snippets.py | 262 ++++++ bigquery/cloud-client/snippets_test.py | 132 +++ bigquery/cloud-client/stream_data.py | 69 ++ bigquery/cloud-client/stream_data_test.py | 29 + bigquery/cloud-client/sync_query.py | 57 ++ bigquery/cloud-client/sync_query_test.py | 27 + bigquery/cloud-client/user_credentials.py | 83 ++ .../cloud-client/user_credentials_test.py | 41 + bigquery/dml/.gitignore | 1 + bigquery/dml/README.rst | 150 +++ bigquery/dml/README.rst.in | 29 + bigquery/dml/insert_sql.py | 78 ++ bigquery/dml/insert_sql_test.py | 35 + bigquery/dml/populate_db.py | 182 ++++ bigquery/dml/populate_db_test.py | 34 + bigquery/dml/requirements.txt | 5 + bigquery/dml/resources/insert_sql_test.sql | 6 + bigquery/rest/README.rst | 105 +++ bigquery/rest/README.rst.in | 23 + bigquery/rest/labels.py | 130 +++ bigquery/rest/labels_test.py | 45 + bigquery/rest/requirements.txt | 2 + bigtable/hello/README.rst | 119 +++ bigtable/hello/README.rst.in | 21 + bigtable/hello/main.py | 117 +++ bigtable/hello/main_test.py | 39 + bigtable/hello/requirements.txt | 2 + bigtable/hello_happybase/README.rst | 126 +++ bigtable/hello_happybase/README.rst.in | 30 + bigtable/hello_happybase/main.py | 114 +++ bigtable/hello_happybase/main_test.py | 41 + bigtable/hello_happybase/requirements.txt | 2 + bigtable/metricscaler/README.rst | 132 +++ bigtable/metricscaler/README.rst.in | 27 + bigtable/metricscaler/metricscaler.py | 163 ++++ bigtable/metricscaler/metricscaler_test.py | 89 ++ bigtable/metricscaler/requirements.txt | 2 + blog/README.md | 5 + .../README.md | 24 + .../blog.py | 129 +++ .../blog_test.py | 25 + .../requirements.txt | 1 + .../wiki.py | 107 +++ .../wiki_test.py | 25 + codelabs/flex_and_vision/README.md | 106 +++ codelabs/flex_and_vision/app.yaml | 9 + codelabs/flex_and_vision/main.py | 128 +++ codelabs/flex_and_vision/requirements.txt | 5 + .../flex_and_vision/templates/homepage.html | 20 + compute/README.md | 24 + compute/api/README.md | 16 + compute/api/create_instance.py | 194 ++++ compute/api/create_instance_test.py | 41 + compute/api/requirements.txt | 1 + compute/api/startup-script.sh | 40 + compute/auth/access_token.py | 78 ++ compute/auth/access_token_test.py | 39 + compute/auth/application_default.py | 60 ++ compute/auth/application_default_test.py | 22 + compute/auth/requirements.txt | 2 + compute/autoscaler/demo/frontend.py | 112 +++ compute/autoscaler/demo/frontend_test.py | 72 ++ .../encryption/generate_wrapped_rsa_key.py | 93 ++ .../generate_wrapped_rsa_key_test.py | 51 ++ compute/encryption/requirements.txt | 2 + compute/metadata/README.md | 10 + compute/metadata/main.py | 81 ++ compute/metadata/main_test.py | 51 ++ compute/metadata/requirements.txt | 1 + compute/xmpp_wikibot/README.md | 75 ++ compute/xmpp_wikibot/requirements.txt | 4 + compute/xmpp_wikibot/wikibot.py | 153 ++++ conftest.py | 38 + container_engine/api-client/README.rst | 97 ++ container_engine/api-client/README.rst.in | 18 + container_engine/api-client/requirements.txt | 3 + container_engine/api-client/snippets.py | 62 ++ container_engine/api-client/snippets_test.py | 22 + .../django_tutorial/.dockerignore | 19 + container_engine/django_tutorial/.gitignore | 1 + container_engine/django_tutorial/Dockerfile | 32 + container_engine/django_tutorial/Makefile | 39 + container_engine/django_tutorial/README.md | 21 + container_engine/django_tutorial/__init__.py | 0 container_engine/django_tutorial/manage.py | 24 + .../django_tutorial/mysite/__init__.py | 0 .../django_tutorial/mysite/settings.py | 116 +++ .../django_tutorial/mysite/urls.py | 28 + .../django_tutorial/mysite/wsgi.py | 22 + container_engine/django_tutorial/polls.yaml | 114 +++ .../django_tutorial/polls/__init__.py | 0 .../django_tutorial/polls/admin.py | 19 + .../django_tutorial/polls/apps.py | 19 + .../django_tutorial/polls/models.py | 28 + .../django_tutorial/polls/tests.py | 13 + .../django_tutorial/polls/urls.py | 21 + .../django_tutorial/polls/views.py | 19 + .../django_tutorial/requirements.txt | 5 + dataproc/README.md | 63 ++ dataproc/create_cluster_and_submit_job.py | 234 +++++ dataproc/dataproc_e2e_test.py | 35 + dataproc/list_clusters.py | 59 ++ dataproc/pyspark_sort.py | 28 + dataproc/requirements.txt | 2 + datastore/cloud-client/README.rst | 136 +++ datastore/cloud-client/README.rst.in | 24 + datastore/cloud-client/index.yaml | 29 + datastore/cloud-client/quickstart.py | 45 + datastore/cloud-client/quickstart_test.py | 21 + datastore/cloud-client/requirements.txt | 1 + datastore/cloud-client/snippets.py | 824 +++++++++++++++++ datastore/cloud-client/snippets_test.py | 285 ++++++ datastore/cloud-client/tasks.py | 141 +++ datastore/cloud-client/tasks_test.py | 91 ++ dns/api/README.rst | 102 +++ dns/api/README.rst.in | 22 + dns/api/main.py | 164 ++++ dns/api/main_test.py | 96 ++ dns/api/requirements.txt | 1 + endpoints/bookstore-grpc/Dockerfile | 22 + endpoints/bookstore-grpc/README.md | 50 + endpoints/bookstore-grpc/api_config.yaml | 45 + endpoints/bookstore-grpc/api_config_auth.yaml | 59 ++ endpoints/bookstore-grpc/api_config_http.yaml | 114 +++ endpoints/bookstore-grpc/bookstore.proto | 126 +++ endpoints/bookstore-grpc/bookstore.py | 74 ++ endpoints/bookstore-grpc/bookstore_client.py | 57 ++ endpoints/bookstore-grpc/bookstore_server.py | 126 +++ .../bookstore-grpc/generated_pb2/__init__.py | 0 .../generated_pb2/bookstore_pb2.py | 855 ++++++++++++++++++ endpoints/bookstore-grpc/http_bookstore.proto | 166 ++++ endpoints/bookstore-grpc/jwt_token_gen.py | 71 ++ endpoints/bookstore-grpc/requirements.txt | 2 + endpoints/bookstore-grpc/status.py | 28 + endpoints/getting-started-grpc/Dockerfile | 21 + endpoints/getting-started-grpc/README.md | 178 ++++ .../getting-started-grpc/api_config.yaml | 36 + .../container-engine.yaml | 54 ++ .../getting-started-grpc/greeter_client.py | 63 ++ .../getting-started-grpc/greeter_server.py | 69 ++ .../getting-started-grpc/helloworld_pb2.py | 259 ++++++ .../helloworld_pb2_grpc.py | 65 ++ .../protos/helloworld.proto | 50 + .../getting-started-grpc/requirements.txt | 2 + endpoints/getting-started/Dockerfile.custom | 16 + endpoints/getting-started/README.md | 165 ++++ endpoints/getting-started/app.yaml | 12 + .../getting-started/clients/echo-client.py | 60 ++ .../clients/google-id-token-client.py | 80 ++ .../clients/google-jwt-client.py | 99 ++ .../service_to_service_gae_default/app.yaml | 7 + .../service_to_service_gae_default/main.py | 83 ++ .../app.yaml | 7 + .../main.py | 97 ++ .../service_to_service_non_default/app.yaml | 7 + .../appengine_config.py | 3 + .../service_to_service_non_default/main.py | 93 ++ .../requirements.txt | 3 + .../getting-started/container-engine.yaml | 56 ++ endpoints/getting-started/main.py | 94 ++ endpoints/getting-started/main_test.py | 82 ++ endpoints/getting-started/openapi.yaml | 160 ++++ endpoints/getting-started/requirements.txt | 8 + endpoints/kubernetes/README.md | 2 + .../kubernetes/grpc-bookstore-server.yaml | 43 + endpoints/kubernetes/grpc-bookstore.yaml | 55 ++ error_reporting/README.rst | 103 +++ error_reporting/README.rst.in | 19 + error_reporting/report_exception.py | 30 + error_reporting/report_exception_test.py | 24 + error_reporting/requirements.txt | 1 + favicon.ico | Bin 0 -> 8348 bytes iap/README.md | 87 ++ iap/app_engine_app/app.yaml | 7 + iap/app_engine_app/appengine_config.py | 18 + iap/app_engine_app/iap_demo.py | 46 + iap/app_engine_app/requirements.txt | 1 + iap/iap_test.py | 50 + iap/make_iap_request.py | 146 +++ iap/requirements.txt | 5 + iap/validate_jwt.py | 83 ++ iot/api-client/README.md | 37 + iot/api-client/generate_keys.sh | 20 + iot/api-client/manager/README.rst | 150 +++ iot/api-client/manager/README.rst.in | 21 + iot/api-client/manager/manager.py | 468 ++++++++++ iot/api-client/manager/manager_test.py | 209 +++++ iot/api-client/manager/requirements.txt | 4 + iot/api-client/manager/resources/README.md | 4 + .../manager/resources/ec_public.pem | 4 + iot/api-client/manager/resources/rsa_cert.pem | 19 + iot/api-client/mqtt_example/README.md | 42 + .../mqtt_example/cloudiot_mqtt_example.py | 211 +++++ iot/api-client/mqtt_example/requirements.txt | 3 + iot/api-client/scripts/README.rst | 115 +++ iot/api-client/scripts/README.rst.in | 22 + iot/api-client/scripts/iam.py | 58 ++ iot/api-client/scripts/requirements.txt | 1 + kms/api-client/README.rst | 109 +++ kms/api-client/README.rst.in | 20 + kms/api-client/quickstart.py | 48 + kms/api-client/quickstart_test.py | 19 + kms/api-client/requirements.txt | 1 + kms/api-client/snippets.py | 371 ++++++++ kms/api-client/snippets_test.py | 154 ++++ language/README.md | 21 + language/api/README.rst | 98 ++ language/api/README.rst.in | 20 + language/api/analyze.py | 102 +++ language/api/analyze_test.py | 259 ++++++ language/api/requirements.txt | 1 + language/cloud-client/v1/README.rst | 139 +++ language/cloud-client/v1/README.rst.in | 24 + language/cloud-client/v1/quickstart.py | 39 + language/cloud-client/v1/quickstart_test.py | 22 + language/cloud-client/v1/requirements.txt | 1 + language/cloud-client/v1/resources/text.txt | 1 + language/cloud-client/v1/snippets.py | 184 ++++ language/cloud-client/v1/snippets_test.py | 58 ++ language/cloud-client/v1beta2/README.rst | 144 +++ language/cloud-client/v1beta2/README.rst.in | 24 + language/cloud-client/v1beta2/quickstart.py | 39 + .../cloud-client/v1beta2/quickstart_test.py | 22 + .../cloud-client/v1beta2/requirements.txt | 2 + .../cloud-client/v1beta2/resources/text.txt | 1 + language/cloud-client/v1beta2/snippets.py | 258 ++++++ .../cloud-client/v1beta2/snippets_test.py | 88 ++ language/movie_nl/README.md | 152 ++++ language/movie_nl/main.py | 334 +++++++ language/movie_nl/main_test.py | 130 +++ language/movie_nl/requirements.txt | 2 + language/ocr_nl/README.md | 227 +++++ language/ocr_nl/main.py | 354 ++++++++ language/ocr_nl/main_test.py | 102 +++ language/ocr_nl/requirements.txt | 1 + language/sentiment/README.md | 48 + language/sentiment/requirements.txt | 1 + language/sentiment/resources/mixed.txt | 20 + language/sentiment/resources/neg.txt | 4 + language/sentiment/resources/neutral.txt | 3 + language/sentiment/resources/pos.txt | 11 + language/sentiment/sentiment_analysis.py | 69 ++ language/sentiment/sentiment_analysis_test.py | 50 + language/syntax_triples/README.md | 91 ++ language/syntax_triples/main.py | 172 ++++ language/syntax_triples/main_test.py | 53 ++ language/syntax_triples/requirements.txt | 1 + .../resources/obama_wikipedia.txt | 1 + language/tutorial/README.rst | 97 ++ language/tutorial/README.rst.in | 20 + language/tutorial/requirements.txt | 1 + .../tutorial/reviews/bladerunner-mixed.txt | 19 + language/tutorial/reviews/bladerunner-neg.txt | 3 + .../tutorial/reviews/bladerunner-neutral.txt | 2 + language/tutorial/reviews/bladerunner-pos.txt | 10 + language/tutorial/tutorial.py | 69 ++ language/tutorial/tutorial_test.py | 51 ++ logging/api-client/README.rst | 87 ++ logging/api-client/README.rst.in | 18 + logging/api-client/list_logs.py | 67 ++ logging/api-client/list_logs_test.py | 25 + logging/api-client/requirements.txt | 1 + logging/cloud-client/README.rst | 163 ++++ logging/cloud-client/README.rst.in | 26 + logging/cloud-client/export.py | 138 +++ logging/cloud-client/export_test.py | 92 ++ logging/cloud-client/quickstart.py | 42 + logging/cloud-client/quickstart_test.py | 22 + logging/cloud-client/requirements.txt | 1 + logging/cloud-client/snippets.py | 97 ++ logging/cloud-client/snippets_test.py | 46 + ml_engine/online_prediction/README.md | 1 + ml_engine/online_prediction/predict.py | 198 ++++ ml_engine/online_prediction/predict_test.py | 71 ++ ml_engine/online_prediction/requirements.txt | 2 + .../resources/census_example_bytes.pb | Bin 0 -> 350 bytes .../resources/census_test_data.json | 1 + monitoring/api/v2/README.rst | 186 ++++ monitoring/api/v2/README.rst.in | 28 + monitoring/api/v2/auth.py | 71 ++ monitoring/api/v2/auth_test.py | 31 + monitoring/api/v2/labeled_custom_metric.py | 192 ++++ .../api/v2/labeled_custom_metric_test.py | 34 + .../api/v2/lightweight_custom_metric.py | 114 +++ .../api/v2/lightweight_custom_metric_test.py | 32 + monitoring/api/v2/requirements.txt | 1 + monitoring/api/v3/api-client/README.rst | 136 +++ monitoring/api/v3/api-client/README.rst.in | 25 + monitoring/api/v3/api-client/custom_metric.py | 207 +++++ .../api/v3/api-client/custom_metric_test.py | 92 ++ .../api/v3/api-client/list_resources.py | 120 +++ .../api/v3/api-client/list_resources_test.py | 70 ++ monitoring/api/v3/api-client/requirements.txt | 1 + monitoring/api/v3/cloud-client/README.rst | 139 +++ monitoring/api/v3/cloud-client/README.rst.in | 26 + monitoring/api/v3/cloud-client/quickstart.py | 42 + .../api/v3/cloud-client/quickstart_test.py | 21 + .../api/v3/cloud-client/requirements.txt | 1 + monitoring/api/v3/cloud-client/snippets.py | 238 +++++ .../api/v3/cloud-client/snippets_test.py | 69 ++ nox.py | 281 ++++++ pubsub/cloud-client/README.rst | 203 +++++ pubsub/cloud-client/README.rst.in | 28 + pubsub/cloud-client/iam.py | 186 ++++ pubsub/cloud-client/iam_test.py | 105 +++ pubsub/cloud-client/publisher.py | 99 ++ pubsub/cloud-client/publisher_test.py | 67 ++ pubsub/cloud-client/quickstart.py | 40 + pubsub/cloud-client/quickstart_test.py | 45 + pubsub/cloud-client/requirements.txt | 1 + pubsub/cloud-client/subscriber.py | 119 +++ pubsub/cloud-client/subscriber_test.py | 87 ++ pytest.ini | 5 + scripts/README.md | 1 + scripts/auto_link_to_docs.py | 146 +++ scripts/decrypt-secrets.sh | 25 + scripts/encrypt-secrets.sh | 30 + scripts/prepare-testing-project.sh | 52 ++ scripts/readme-gen/readme_gen.py | 66 ++ scripts/readme-gen/templates/README.tmpl.rst | 67 ++ scripts/readme-gen/templates/auth.tmpl.rst | 33 + .../templates/auth_api_key.tmpl.rst | 14 + .../templates/install_deps.tmpl.rst | 20 + .../templates/install_portaudio.tmpl.rst | 35 + scripts/resources/docs-links.json | 377 ++++++++ scripts/travis.sh | 14 + spanner/cloud-client/README.rst | 175 ++++ spanner/cloud-client/README.rst.in | 22 + spanner/cloud-client/quickstart.py | 47 + spanner/cloud-client/quickstart_test.py | 59 ++ spanner/cloud-client/requirements.txt | 1 + spanner/cloud-client/snippets.py | 470 ++++++++++ spanner/cloud-client/snippets_test.py | 171 ++++ speech/cloud-client/README.rst | 181 ++++ speech/cloud-client/README.rst.in | 29 + speech/cloud-client/quickstart.py | 53 ++ speech/cloud-client/quickstart_test.py | 22 + speech/cloud-client/requirements.txt | 1 + speech/cloud-client/resources/audio.raw | Bin 0 -> 57958 bytes speech/cloud-client/resources/audio2.raw | Bin 0 -> 160000 bytes speech/cloud-client/transcribe.py | 75 ++ speech/cloud-client/transcribe_async.py | 102 +++ speech/cloud-client/transcribe_async_test.py | 35 + speech/cloud-client/transcribe_streaming.py | 54 ++ .../cloud-client/transcribe_streaming_test.py | 27 + speech/cloud-client/transcribe_test.py | 34 + storage/api/README.rst | 206 +++++ storage/api/README.rst.in | 27 + storage/api/compose_objects.py | 97 ++ storage/api/compose_objects_test.py | 28 + storage/api/crud_object.py | 139 +++ storage/api/crud_object_test.py | 27 + storage/api/customer_supplied_keys.py | 158 ++++ storage/api/customer_supplied_keys_test.py | 30 + storage/api/list_objects.py | 83 ++ storage/api/list_objects_test.py | 22 + storage/api/requirements.txt | 1 + storage/api/resources/file1.txt | 1 + storage/api/resources/file2.txt | 1 + storage/cloud-client/README.rst | 289 ++++++ storage/cloud-client/README.rst.in | 31 + storage/cloud-client/acl.py | 274 ++++++ storage/cloud-client/acl_test.py | 138 +++ storage/cloud-client/encryption.py | 177 ++++ storage/cloud-client/encryption_test.py | 93 ++ storage/cloud-client/iam.py | 95 ++ storage/cloud-client/iam_test.py | 45 + storage/cloud-client/notification_polling.py | 117 +++ .../cloud-client/notification_polling_test.py | 47 + storage/cloud-client/quickstart.py | 37 + storage/cloud-client/quickstart_test.py | 28 + storage/cloud-client/requirements.txt | 2 + storage/cloud-client/snippets.py | 300 ++++++ storage/cloud-client/snippets_test.py | 130 +++ storage/transfer_service/README.rst | 179 ++++ storage/transfer_service/README.rst.in | 28 + storage/transfer_service/aws_request.py | 112 +++ storage/transfer_service/create_client.py | 20 + storage/transfer_service/nearline_request.py | 104 +++ storage/transfer_service/requirements.txt | 1 + storage/transfer_service/transfer_check.py | 61 ++ testing/.gitignore | 3 + testing/requirements.txt | 17 + testing/secrets.tar.enc | Bin 0 -> 9760 bytes testing/test-env.tmpl.sh | 23 + translate/cloud-client/README.rst | 141 +++ translate/cloud-client/README.rst.in | 22 + translate/cloud-client/quickstart.py | 42 + translate/cloud-client/quickstart_test.py | 23 + translate/cloud-client/requirements.txt | 1 + translate/cloud-client/snippets.py | 144 +++ translate/cloud-client/snippets_test.py | 49 + video/cloud-client/analyze/README.rst | 126 +++ video/cloud-client/analyze/README.rst.in | 20 + video/cloud-client/analyze/analyze.py | 237 +++++ video/cloud-client/analyze/analyze_test.py | 60 ++ video/cloud-client/analyze/requirements.txt | 1 + .../cloud-client/analyze/resources/README.md | 12 + video/cloud-client/faces/README.rst | 120 +++ video/cloud-client/faces/README.rst.in | 20 + video/cloud-client/faces/faces.py | 86 ++ video/cloud-client/faces/faces_test.py | 32 + video/cloud-client/faces/requirements.txt | 1 + video/cloud-client/labels/README.rst | 120 +++ video/cloud-client/labels/README.rst.in | 20 + video/cloud-client/labels/labels.py | 85 ++ video/cloud-client/labels/labels_test.py | 32 + video/cloud-client/labels/requirements.txt | 1 + video/cloud-client/quickstart/README.rst | 102 +++ video/cloud-client/quickstart/README.rst.in | 19 + video/cloud-client/quickstart/quickstart.py | 67 ++ .../quickstart/quickstart_test.py | 26 + .../cloud-client/quickstart/requirements.txt | 1 + video/cloud-client/shotchange/README.rst | 120 +++ video/cloud-client/shotchange/README.rst.in | 20 + .../cloud-client/shotchange/requirements.txt | 1 + video/cloud-client/shotchange/shotchange.py | 81 ++ .../shotchange/shotchange_test.py | 32 + vision/api/label/README.rst | 96 ++ vision/api/label/README.rst.in | 24 + vision/api/label/label.py | 72 ++ vision/api/label/label_test.py | 28 + vision/api/label/requirements.txt | 1 + vision/api/label/resources/cat.jpg | Bin 0 -> 122667 bytes vision/api/label/snippets.py | 89 ++ vision/api/label/snippets_test.py | 50 + vision/cloud-client/crop_hints/.gitignore | 2 + vision/cloud-client/crop_hints/README.rst | 111 +++ vision/cloud-client/crop_hints/README.rst.in | 22 + vision/cloud-client/crop_hints/crop_hints.py | 90 ++ .../crop_hints/crop_hints_test.py | 37 + .../cloud-client/crop_hints/requirements.txt | 2 + .../crop_hints/resources/cropme.jpg | Bin 0 -> 63856 bytes vision/cloud-client/detect/README.rst | 155 ++++ vision/cloud-client/detect/README.rst.in | 22 + vision/cloud-client/detect/detect.py | 581 ++++++++++++ vision/cloud-client/detect/detect_test.py | 251 +++++ vision/cloud-client/detect/requirements.txt | 1 + .../detect/resources/face_no_surprise.jpg | Bin 0 -> 93301 bytes .../detect/resources/landmark.jpg | Bin 0 -> 161448 bytes .../cloud-client/detect/resources/logos.png | Bin 0 -> 8250 bytes vision/cloud-client/detect/resources/text.jpg | Bin 0 -> 124744 bytes .../detect/resources/wakeupcat.jpg | Bin 0 -> 64892 bytes vision/cloud-client/document_text/.gitignore | 1 + vision/cloud-client/document_text/README.rst | 111 +++ .../cloud-client/document_text/README.rst.in | 22 + vision/cloud-client/document_text/doctext.py | 121 +++ .../document_text/doctext_test.py | 24 + .../document_text/requirements.txt | 2 + .../document_text/resources/text_menu.jpg | Bin 0 -> 53217 bytes vision/cloud-client/face_detection/.gitignore | 1 + vision/cloud-client/face_detection/README.rst | 101 +++ .../cloud-client/face_detection/README.rst.in | 23 + vision/cloud-client/face_detection/faces.py | 88 ++ .../cloud-client/face_detection/faces_test.py | 39 + .../face_detection/requirements.txt | 2 + .../face_detection/resources/face-input.jpg | Bin 0 -> 68133 bytes vision/cloud-client/quickstart/README.rst | 110 +++ vision/cloud-client/quickstart/README.rst.in | 22 + vision/cloud-client/quickstart/quickstart.py | 50 + .../quickstart/quickstart_test.py | 21 + .../cloud-client/quickstart/requirements.txt | 1 + .../quickstart/resources/wakeupcat.jpg | Bin 0 -> 64892 bytes vision/cloud-client/web/README.rst | 118 +++ vision/cloud-client/web/README.rst.in | 22 + vision/cloud-client/web/requirements.txt | 1 + vision/cloud-client/web/web_detect.py | 100 ++ vision/cloud-client/web/web_detect_test.py | 40 + 1059 files changed, 56594 insertions(+) create mode 100644 .coveragerc create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 AUTHORING_GUIDE.md create mode 100644 CONTRIBUTING.md create mode 100644 ISSUE_TEMPLATE.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 appengine/flexible/.gitignore create mode 100644 appengine/flexible/README.md create mode 100644 appengine/flexible/analytics/README.md create mode 100644 appengine/flexible/analytics/app.yaml create mode 100644 appengine/flexible/analytics/main.py create mode 100644 appengine/flexible/analytics/main_test.py create mode 100644 appengine/flexible/analytics/requirements.txt create mode 100644 appengine/flexible/cloudsql/README.md create mode 100644 appengine/flexible/cloudsql/app.yaml create mode 100755 appengine/flexible/cloudsql/create_tables.py create mode 100644 appengine/flexible/cloudsql/main.py create mode 100644 appengine/flexible/cloudsql/main_test.py create mode 100644 appengine/flexible/cloudsql/requirements.txt create mode 100644 appengine/flexible/datastore/README.md create mode 100644 appengine/flexible/datastore/app.yaml create mode 100644 appengine/flexible/datastore/main.py create mode 100644 appengine/flexible/datastore/main_test.py create mode 100644 appengine/flexible/datastore/requirements.txt create mode 100644 appengine/flexible/disk/app.yaml create mode 100644 appengine/flexible/disk/main.py create mode 100644 appengine/flexible/disk/main_test.py create mode 100644 appengine/flexible/disk/requirements.txt create mode 100644 appengine/flexible/django_cloudsql/README.md create mode 100644 appengine/flexible/django_cloudsql/app.yaml create mode 100755 appengine/flexible/django_cloudsql/manage.py create mode 100644 appengine/flexible/django_cloudsql/mysite/__init__.py create mode 100644 appengine/flexible/django_cloudsql/mysite/settings.py create mode 100644 appengine/flexible/django_cloudsql/mysite/urls.py create mode 100644 appengine/flexible/django_cloudsql/mysite/wsgi.py create mode 100644 appengine/flexible/django_cloudsql/polls/__init__.py create mode 100644 appengine/flexible/django_cloudsql/polls/admin.py create mode 100644 appengine/flexible/django_cloudsql/polls/apps.py create mode 100644 appengine/flexible/django_cloudsql/polls/models.py create mode 100644 appengine/flexible/django_cloudsql/polls/tests.py create mode 100644 appengine/flexible/django_cloudsql/polls/urls.py create mode 100644 appengine/flexible/django_cloudsql/polls/views.py create mode 100644 appengine/flexible/django_cloudsql/requirements.txt create mode 100644 appengine/flexible/extending_runtime/.dockerignore create mode 100644 appengine/flexible/extending_runtime/Dockerfile create mode 100644 appengine/flexible/extending_runtime/app.yaml create mode 100644 appengine/flexible/extending_runtime/main.py create mode 100644 appengine/flexible/extending_runtime/main_test.py create mode 100644 appengine/flexible/extending_runtime/requirements.txt create mode 100644 appengine/flexible/hello_world/app.yaml create mode 100644 appengine/flexible/hello_world/main.py create mode 100644 appengine/flexible/hello_world/main_test.py create mode 100644 appengine/flexible/hello_world/requirements.txt create mode 100644 appengine/flexible/hello_world_django/.gitignore create mode 100644 appengine/flexible/hello_world_django/README.md create mode 100644 appengine/flexible/hello_world_django/app.yaml create mode 100644 appengine/flexible/hello_world_django/helloworld/__init__.py create mode 100644 appengine/flexible/hello_world_django/helloworld/views.py create mode 100755 appengine/flexible/hello_world_django/manage.py create mode 100644 appengine/flexible/hello_world_django/project_name/__init__.py create mode 100644 appengine/flexible/hello_world_django/project_name/settings.py create mode 100644 appengine/flexible/hello_world_django/project_name/urls.py create mode 100644 appengine/flexible/hello_world_django/project_name/wsgi.py create mode 100644 appengine/flexible/hello_world_django/requirements.txt create mode 100644 appengine/flexible/mailgun/README.md create mode 100644 appengine/flexible/mailgun/app.yaml create mode 100644 appengine/flexible/mailgun/example-attachment.txt create mode 100644 appengine/flexible/mailgun/main.py create mode 100644 appengine/flexible/mailgun/main_test.py create mode 100644 appengine/flexible/mailgun/requirements.txt create mode 100644 appengine/flexible/mailgun/templates/index.html create mode 100644 appengine/flexible/mailjet/README.md create mode 100644 appengine/flexible/mailjet/app.yaml create mode 100644 appengine/flexible/mailjet/main.py create mode 100644 appengine/flexible/mailjet/main_test.py create mode 100644 appengine/flexible/mailjet/requirements.txt create mode 100644 appengine/flexible/mailjet/templates/index.html create mode 100644 appengine/flexible/memcache/README.md create mode 100644 appengine/flexible/memcache/app.yaml create mode 100644 appengine/flexible/memcache/main.py create mode 100644 appengine/flexible/memcache/main_test.py create mode 100644 appengine/flexible/memcache/requirements.txt create mode 100644 appengine/flexible/metadata/app.yaml create mode 100644 appengine/flexible/metadata/main.py create mode 100644 appengine/flexible/metadata/requirements.txt create mode 100644 appengine/flexible/multiple_services/README.md create mode 100644 appengine/flexible/multiple_services/gateway-service/app.yaml create mode 100644 appengine/flexible/multiple_services/gateway-service/main.py create mode 100644 appengine/flexible/multiple_services/gateway-service/requirements.txt create mode 100644 appengine/flexible/multiple_services/gateway-service/services_config.py create mode 100644 appengine/flexible/multiple_services/static-service/app.yaml create mode 100644 appengine/flexible/multiple_services/static-service/main.py create mode 100644 appengine/flexible/multiple_services/static-service/requirements.txt create mode 100644 appengine/flexible/multiple_services/static-service/static/index.html create mode 100644 appengine/flexible/multiple_services/static-service/static/index.js create mode 100644 appengine/flexible/multiple_services/static-service/static/style.css create mode 100644 appengine/flexible/numpy/app.yaml create mode 100644 appengine/flexible/numpy/main.py create mode 100644 appengine/flexible/numpy/main_test.py create mode 100644 appengine/flexible/numpy/requirements.txt 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.txt create mode 100644 appengine/flexible/pubsub/sample_message.json create mode 100644 appengine/flexible/pubsub/templates/index.html create mode 100644 appengine/flexible/redis/app.yaml create mode 100644 appengine/flexible/redis/main.py create mode 100644 appengine/flexible/redis/main_test.py create mode 100644 appengine/flexible/redis/requirements.txt create mode 100644 appengine/flexible/scipy/.gitignore create mode 100644 appengine/flexible/scipy/README.md create mode 100644 appengine/flexible/scipy/app.yaml create mode 100644 appengine/flexible/scipy/assets/google_logo.jpg create mode 100644 appengine/flexible/scipy/fixtures/assets/resized_google_logo.jpg create mode 100644 appengine/flexible/scipy/main.py create mode 100644 appengine/flexible/scipy/main_test.py create mode 100644 appengine/flexible/scipy/requirements.txt create mode 100644 appengine/flexible/sendgrid/README.md create mode 100644 appengine/flexible/sendgrid/app.yaml create mode 100644 appengine/flexible/sendgrid/main.py create mode 100644 appengine/flexible/sendgrid/main_test.py create mode 100644 appengine/flexible/sendgrid/requirements.txt create mode 100644 appengine/flexible/sendgrid/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.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/storage/README.md create mode 100644 appengine/flexible/storage/app.yaml create mode 100644 appengine/flexible/storage/main.py create mode 100644 appengine/flexible/storage/main_test.py create mode 100644 appengine/flexible/storage/requirements.txt create mode 100644 appengine/flexible/twilio/README.md create mode 100644 appengine/flexible/twilio/app.yaml create mode 100644 appengine/flexible/twilio/main.py create mode 100644 appengine/flexible/twilio/main_test.py create mode 100644 appengine/flexible/twilio/requirements.txt create mode 100644 appengine/standard/README.md create mode 100644 appengine/standard/analytics/app.yaml create mode 100644 appengine/standard/analytics/appengine_config.py create mode 100644 appengine/standard/analytics/main.py create mode 100644 appengine/standard/analytics/main_test.py create mode 100644 appengine/standard/analytics/requirements.txt create mode 100644 appengine/standard/angular/README.md create mode 100644 appengine/standard/angular/app.yaml create mode 100644 appengine/standard/angular/app/css/app.css create mode 100644 appengine/standard/angular/app/index.html create mode 100644 appengine/standard/angular/app/js/app.js create mode 100644 appengine/standard/angular/app/partials/insert.html create mode 100644 appengine/standard/angular/app/partials/main.html create mode 100644 appengine/standard/angular/app/partials/update.html create mode 100644 appengine/standard/angular/main.py create mode 100644 appengine/standard/angular/model.py create mode 100755 appengine/standard/angular/scripts/deploy.sh create mode 100755 appengine/standard/angular/scripts/run.sh create mode 100644 appengine/standard/app_identity/asserting/app.yaml create mode 100644 appengine/standard/app_identity/asserting/main.py create mode 100644 appengine/standard/app_identity/asserting/main_test.py create mode 100644 appengine/standard/app_identity/incoming/app.yaml create mode 100644 appengine/standard/app_identity/incoming/main.py create mode 100644 appengine/standard/app_identity/incoming/main_test.py create mode 100644 appengine/standard/app_identity/signing/app.yaml create mode 100644 appengine/standard/app_identity/signing/main.py create mode 100644 appengine/standard/app_identity/signing/main_test.py create mode 100644 appengine/standard/appstats/app.yaml create mode 100644 appengine/standard/appstats/appengine_config.py create mode 100644 appengine/standard/appstats/main.py create mode 100644 appengine/standard/appstats/main_test.py create mode 100644 appengine/standard/background/README.md create mode 100644 appengine/standard/background/app.yaml create mode 100644 appengine/standard/background/main.py create mode 100644 appengine/standard/background/main_test.py create mode 100644 appengine/standard/bigquery/.gitignore create mode 100644 appengine/standard/bigquery/README.md create mode 100644 appengine/standard/bigquery/app.yaml create mode 100644 appengine/standard/bigquery/appengine_config.py create mode 100644 appengine/standard/bigquery/client_secrets.json create mode 100644 appengine/standard/bigquery/main.py create mode 100644 appengine/standard/bigquery/main_test.py create mode 100644 appengine/standard/bigquery/requirements.txt create mode 100644 appengine/standard/bigquery/resources/datasets-list.json create mode 100644 appengine/standard/blobstore/api/README.md create mode 100644 appengine/standard/blobstore/api/app.yaml create mode 100644 appengine/standard/blobstore/api/main.py create mode 100644 appengine/standard/blobstore/api/main_test.py create mode 100644 appengine/standard/blobstore/blobreader/app.yaml create mode 100644 appengine/standard/blobstore/blobreader/appengine_config.py create mode 100644 appengine/standard/blobstore/blobreader/main.py create mode 100644 appengine/standard/blobstore/blobreader/main_test.py create mode 100644 appengine/standard/blobstore/blobreader/requirements.txt create mode 100644 appengine/standard/blobstore/gcs/app.yaml create mode 100644 appengine/standard/blobstore/gcs/appengine_config.py create mode 100644 appengine/standard/blobstore/gcs/main.py create mode 100644 appengine/standard/blobstore/gcs/main_test.py create mode 100644 appengine/standard/blobstore/gcs/requirements.txt create mode 100644 appengine/standard/channel/README.md create mode 100644 appengine/standard/channel/app.yaml create mode 100644 appengine/standard/channel/chatactoe.py create mode 100644 appengine/standard/channel/index.html create mode 100644 appengine/standard/cloudsql/README.md create mode 100644 appengine/standard/cloudsql/app.yaml create mode 100644 appengine/standard/cloudsql/main.py create mode 100644 appengine/standard/cloudsql/main_test.py create mode 100644 appengine/standard/conftest.py create mode 100644 appengine/standard/django/README.md create mode 100644 appengine/standard/django/app.yaml create mode 100644 appengine/standard/django/appengine_config.py create mode 100755 appengine/standard/django/manage.py create mode 100644 appengine/standard/django/mysite/__init__.py create mode 100644 appengine/standard/django/mysite/settings.py create mode 100644 appengine/standard/django/mysite/urls.py create mode 100644 appengine/standard/django/mysite/wsgi.py create mode 100644 appengine/standard/django/polls/__init__.py create mode 100644 appengine/standard/django/polls/admin.py create mode 100644 appengine/standard/django/polls/models.py create mode 100644 appengine/standard/django/polls/tests.py create mode 100644 appengine/standard/django/polls/views.py create mode 100644 appengine/standard/django/requirements-vendor.txt create mode 100644 appengine/standard/django/requirements.txt create mode 100644 appengine/standard/endpoints-frameworks-v2/echo/README.md create mode 100644 appengine/standard/endpoints-frameworks-v2/echo/app.yaml create mode 100644 appengine/standard/endpoints-frameworks-v2/echo/appengine_config.py create mode 100644 appengine/standard/endpoints-frameworks-v2/echo/main.py create mode 100644 appengine/standard/endpoints-frameworks-v2/echo/main_test.py create mode 100644 appengine/standard/endpoints-frameworks-v2/echo/requirements.txt create mode 100644 appengine/standard/endpoints-frameworks-v2/quickstart/app.yaml create mode 100644 appengine/standard/endpoints-frameworks-v2/quickstart/appengine_config.py create mode 100644 appengine/standard/endpoints-frameworks-v2/quickstart/main.py create mode 100644 appengine/standard/endpoints-frameworks-v2/quickstart/main_test.py create mode 100644 appengine/standard/endpoints/backend/app.yaml create mode 100644 appengine/standard/endpoints/backend/main.py create mode 100644 appengine/standard/endpoints/backend/main_test.py create mode 100644 appengine/standard/endpoints/multiapi/app.yaml create mode 100644 appengine/standard/endpoints/multiapi/main.py create mode 100644 appengine/standard/endpoints/multiapi/main_test.py create mode 100644 appengine/standard/firebase/firenotes/README.md create mode 100644 appengine/standard/firebase/firenotes/backend/.gitignore create mode 100644 appengine/standard/firebase/firenotes/backend/app.yaml create mode 100644 appengine/standard/firebase/firenotes/backend/appengine_config.py create mode 100644 appengine/standard/firebase/firenotes/backend/index.yaml create mode 100644 appengine/standard/firebase/firenotes/backend/main.py create mode 100644 appengine/standard/firebase/firenotes/backend/main_test.py create mode 100644 appengine/standard/firebase/firenotes/backend/requirements.txt create mode 100644 appengine/standard/firebase/firenotes/frontend/app.yaml create mode 100644 appengine/standard/firebase/firenotes/frontend/index.html create mode 100644 appengine/standard/firebase/firenotes/frontend/main.js create mode 100644 appengine/standard/firebase/firenotes/frontend/style.css create mode 100644 appengine/standard/firebase/firetactoe/README.md create mode 100644 appengine/standard/firebase/firetactoe/app.yaml create mode 100644 appengine/standard/firebase/firetactoe/appengine_config.py create mode 100644 appengine/standard/firebase/firetactoe/firetactoe.py create mode 100644 appengine/standard/firebase/firetactoe/firetactoe_test.py create mode 100644 appengine/standard/firebase/firetactoe/requirements.txt create mode 100644 appengine/standard/firebase/firetactoe/rest_api.py create mode 100644 appengine/standard/firebase/firetactoe/static/main.css create mode 100644 appengine/standard/firebase/firetactoe/static/main.js create mode 100644 appengine/standard/firebase/firetactoe/templates/_firebase_config.html create mode 100644 appengine/standard/firebase/firetactoe/templates/fire_index.html create mode 100644 appengine/standard/flask/hello_world/.gitignore create mode 100644 appengine/standard/flask/hello_world/README.md create mode 100644 appengine/standard/flask/hello_world/app.yaml create mode 100644 appengine/standard/flask/hello_world/appengine_config.py create mode 100644 appengine/standard/flask/hello_world/main.py create mode 100644 appengine/standard/flask/hello_world/main_test.py create mode 100644 appengine/standard/flask/hello_world/requirements.txt create mode 100644 appengine/standard/flask/tutorial/.gitignore create mode 100644 appengine/standard/flask/tutorial/README.md create mode 100644 appengine/standard/flask/tutorial/app.yaml create mode 100644 appengine/standard/flask/tutorial/appengine_config.py create mode 100644 appengine/standard/flask/tutorial/main.py create mode 100644 appengine/standard/flask/tutorial/main_test.py create mode 100644 appengine/standard/flask/tutorial/requirements.txt create mode 100644 appengine/standard/flask/tutorial/static/style.css create mode 100644 appengine/standard/flask/tutorial/templates/form.html create mode 100644 appengine/standard/flask/tutorial/templates/submitted_form.html create mode 100644 appengine/standard/hello_world/app.yaml create mode 100644 appengine/standard/hello_world/main.py create mode 100644 appengine/standard/hello_world/main_test.py create mode 100644 appengine/standard/i18n/README.md create mode 100644 appengine/standard/i18n/app.yaml create mode 100644 appengine/standard/i18n/appengine_config.py create mode 100644 appengine/standard/i18n/i18n_utils.py create mode 100644 appengine/standard/i18n/js.mapping create mode 100644 appengine/standard/i18n/locales/en/LC_MESSAGES/jsmessages.mo create mode 100644 appengine/standard/i18n/locales/en/LC_MESSAGES/jsmessages.po create mode 100644 appengine/standard/i18n/locales/en/LC_MESSAGES/messages.mo create mode 100644 appengine/standard/i18n/locales/en/LC_MESSAGES/messages.po create mode 100644 appengine/standard/i18n/locales/ja/LC_MESSAGES/jsmessages.mo create mode 100644 appengine/standard/i18n/locales/ja/LC_MESSAGES/jsmessages.po create mode 100644 appengine/standard/i18n/locales/ja/LC_MESSAGES/messages.mo create mode 100644 appengine/standard/i18n/locales/ja/LC_MESSAGES/messages.po create mode 100644 appengine/standard/i18n/locales/jsmessages.pot create mode 100644 appengine/standard/i18n/locales/messages.pot create mode 100644 appengine/standard/i18n/locales/pl/LC_MESSAGES/jsmessages.mo create mode 100644 appengine/standard/i18n/locales/pl/LC_MESSAGES/jsmessages.po create mode 100644 appengine/standard/i18n/main.mapping create mode 100644 appengine/standard/i18n/main.py create mode 100644 appengine/standard/i18n/static/js/main.js create mode 100644 appengine/standard/i18n/templates/i18n_js.jinja2 create mode 100644 appengine/standard/i18n/templates/index.jinja2 create mode 100644 appengine/standard/i18n/templates/javascript_tag.jinja2 create mode 100644 appengine/standard/i18n/templates/null_i18n_js.jinja2 create mode 100644 appengine/standard/images/api/README.md create mode 100644 appengine/standard/images/api/app.yaml create mode 100644 appengine/standard/images/api/blobstore.py create mode 100644 appengine/standard/images/api/blobstore_test.py create mode 100644 appengine/standard/images/api/favicon.ico create mode 100644 appengine/standard/images/api/main.py create mode 100644 appengine/standard/images/api/main_test.py create mode 100644 appengine/standard/images/guestbook/README.md create mode 100644 appengine/standard/images/guestbook/app.yaml create mode 100644 appengine/standard/images/guestbook/favicon.ico create mode 100644 appengine/standard/images/guestbook/index.yaml create mode 100644 appengine/standard/images/guestbook/main.py create mode 100644 appengine/standard/images/guestbook/main_test.py create mode 100644 appengine/standard/localtesting/README.md create mode 100644 appengine/standard/localtesting/datastore_test.py create mode 100644 appengine/standard/localtesting/env_vars_test.py create mode 100644 appengine/standard/localtesting/login_test.py create mode 100644 appengine/standard/localtesting/mail_test.py create mode 100644 appengine/standard/localtesting/queue.yaml create mode 100644 appengine/standard/localtesting/resources/queue.yaml create mode 100755 appengine/standard/localtesting/runner.py create mode 100644 appengine/standard/localtesting/task_queue_test.py create mode 100644 appengine/standard/logging/reading_logs/app.yaml create mode 100644 appengine/standard/logging/reading_logs/main.py create mode 100644 appengine/standard/logging/reading_logs/main_test.py create mode 100644 appengine/standard/logging/writing_logs/app.yaml create mode 100644 appengine/standard/logging/writing_logs/main.py create mode 100644 appengine/standard/logging/writing_logs/main_test.py create mode 100644 appengine/standard/mail/README.md create mode 100644 appengine/standard/mail/app.yaml create mode 100644 appengine/standard/mail/attachment.py create mode 100644 appengine/standard/mail/attachment_test.py create mode 100644 appengine/standard/mail/handle_bounced_email.py create mode 100644 appengine/standard/mail/handle_bounced_email_test.py create mode 100644 appengine/standard/mail/handle_incoming_email.py create mode 100644 appengine/standard/mail/handle_incoming_email_test.py create mode 100644 appengine/standard/mail/header.py create mode 100644 appengine/standard/mail/header_test.py create mode 100644 appengine/standard/mail/index.html create mode 100644 appengine/standard/mail/send_mail.py create mode 100644 appengine/standard/mail/send_mail_test.py create mode 100644 appengine/standard/mail/send_message.py create mode 100644 appengine/standard/mail/send_message_test.py create mode 100644 appengine/standard/mail/user_signup.py create mode 100644 appengine/standard/mail/user_signup_test.py create mode 100644 appengine/standard/mailgun/.gitignore create mode 100644 appengine/standard/mailgun/README.md create mode 100644 appengine/standard/mailgun/app.yaml create mode 100644 appengine/standard/mailgun/appengine_config.py create mode 100644 appengine/standard/mailgun/main.py create mode 100644 appengine/standard/mailgun/main_test.py create mode 100644 appengine/standard/mailgun/requirements.txt create mode 100644 appengine/standard/mailjet/.gitignore create mode 100644 appengine/standard/mailjet/README.md create mode 100644 appengine/standard/mailjet/app.yaml create mode 100644 appengine/standard/mailjet/appengine_config.py create mode 100644 appengine/standard/mailjet/main.py create mode 100644 appengine/standard/mailjet/main_test.py create mode 100644 appengine/standard/mailjet/requirements.txt create mode 100644 appengine/standard/mailjet/templates/index.html create mode 100644 appengine/standard/memcache/best_practices/README.md create mode 100644 appengine/standard/memcache/best_practices/batch/app.yaml create mode 100644 appengine/standard/memcache/best_practices/batch/batch.py create mode 100644 appengine/standard/memcache/best_practices/batch/batch_test.py create mode 100644 appengine/standard/memcache/best_practices/failure/app.yaml create mode 100644 appengine/standard/memcache/best_practices/failure/failure.py create mode 100644 appengine/standard/memcache/best_practices/failure/failure_test.py create mode 100644 appengine/standard/memcache/best_practices/migration_step1/app.yaml create mode 100644 appengine/standard/memcache/best_practices/migration_step1/migration1.py create mode 100644 appengine/standard/memcache/best_practices/migration_step1/migration1_test.py create mode 100644 appengine/standard/memcache/best_practices/migration_step2/app.yaml create mode 100644 appengine/standard/memcache/best_practices/migration_step2/migration2.py create mode 100644 appengine/standard/memcache/best_practices/migration_step2/migration2_test.py create mode 100644 appengine/standard/memcache/best_practices/sharing/app.yaml create mode 100644 appengine/standard/memcache/best_practices/sharing/sharing.py create mode 100644 appengine/standard/memcache/best_practices/sharing/sharing_test.py create mode 100644 appengine/standard/memcache/guestbook/README.md create mode 100644 appengine/standard/memcache/guestbook/app.yaml create mode 100644 appengine/standard/memcache/guestbook/favicon.ico create mode 100644 appengine/standard/memcache/guestbook/index.yaml create mode 100644 appengine/standard/memcache/guestbook/main.py create mode 100644 appengine/standard/memcache/guestbook/main_test.py create mode 100644 appengine/standard/memcache/snippets/snippets.py create mode 100644 appengine/standard/memcache/snippets/snippets_test.py create mode 100644 appengine/standard/modules/README.md create mode 100644 appengine/standard/modules/app.yaml create mode 100644 appengine/standard/modules/backend.py create mode 100644 appengine/standard/modules/backend.yaml create mode 100644 appengine/standard/modules/backend_test.py create mode 100644 appengine/standard/modules/main.py create mode 100644 appengine/standard/modules/main_test.py create mode 100644 appengine/standard/multitenancy/README.md create mode 100644 appengine/standard/multitenancy/app.yaml create mode 100644 appengine/standard/multitenancy/datastore.py create mode 100644 appengine/standard/multitenancy/datastore_test.py create mode 100644 appengine/standard/multitenancy/memcache.py create mode 100644 appengine/standard/multitenancy/memcache_test.py create mode 100644 appengine/standard/multitenancy/taskqueue.py create mode 100644 appengine/standard/multitenancy/taskqueue_test.py create mode 100644 appengine/standard/ndb/async/README.md create mode 100644 appengine/standard/ndb/async/app_async.py create mode 100644 appengine/standard/ndb/async/app_async_test.py create mode 100644 appengine/standard/ndb/async/app_sync.py create mode 100644 appengine/standard/ndb/async/app_sync_test.py create mode 100644 appengine/standard/ndb/async/app_toplevel/README.md create mode 100644 appengine/standard/ndb/async/app_toplevel/app_toplevel.py create mode 100644 appengine/standard/ndb/async/app_toplevel/app_toplevel_test.py create mode 100644 appengine/standard/ndb/async/app_toplevel/index.html create mode 100644 appengine/standard/ndb/async/guestbook.py create mode 100644 appengine/standard/ndb/async/guestbook_test.py create mode 100644 appengine/standard/ndb/async/shopping_cart.py create mode 100644 appengine/standard/ndb/async/shopping_cart_test.py create mode 100644 appengine/standard/ndb/cache/README.md create mode 100644 appengine/standard/ndb/cache/snippets.py create mode 100644 appengine/standard/ndb/cache/snippets_test.py create mode 100644 appengine/standard/ndb/entities/README.md create mode 100644 appengine/standard/ndb/entities/snippets.py create mode 100644 appengine/standard/ndb/entities/snippets_test.py create mode 100644 appengine/standard/ndb/modeling/README.md create mode 100644 appengine/standard/ndb/modeling/contact_with_group_models.py create mode 100644 appengine/standard/ndb/modeling/contact_with_group_models_test.py create mode 100644 appengine/standard/ndb/modeling/keyproperty_models.py create mode 100644 appengine/standard/ndb/modeling/keyproperty_models_test.py create mode 100644 appengine/standard/ndb/modeling/naive_models.py create mode 100644 appengine/standard/ndb/modeling/naive_models_test.py create mode 100644 appengine/standard/ndb/modeling/parent_child_models.py create mode 100644 appengine/standard/ndb/modeling/parent_child_models_test.py create mode 100644 appengine/standard/ndb/modeling/relation_model_models.py create mode 100644 appengine/standard/ndb/modeling/relation_model_models_test.py create mode 100644 appengine/standard/ndb/modeling/structured_property_models.py create mode 100644 appengine/standard/ndb/modeling/structured_property_models_test.py create mode 100644 appengine/standard/ndb/overview/README.md create mode 100644 appengine/standard/ndb/overview/app.yaml create mode 100644 appengine/standard/ndb/overview/favicon.ico create mode 100644 appengine/standard/ndb/overview/index.yaml create mode 100644 appengine/standard/ndb/overview/main.py create mode 100644 appengine/standard/ndb/overview/main_test.py create mode 100644 appengine/standard/ndb/projection_queries/README.md create mode 100644 appengine/standard/ndb/projection_queries/snippets.py create mode 100644 appengine/standard/ndb/projection_queries/snippets_test.py create mode 100644 appengine/standard/ndb/properties/README.md create mode 100644 appengine/standard/ndb/properties/snippets.py create mode 100644 appengine/standard/ndb/properties/snippets_test.py create mode 100644 appengine/standard/ndb/property_subclasses/README.md create mode 100644 appengine/standard/ndb/property_subclasses/my_models.py create mode 100644 appengine/standard/ndb/property_subclasses/snippets.py create mode 100644 appengine/standard/ndb/property_subclasses/snippets_test.py create mode 100644 appengine/standard/ndb/queries/README.md create mode 100644 appengine/standard/ndb/queries/guestbook.py create mode 100644 appengine/standard/ndb/queries/guestbook_test.py create mode 100644 appengine/standard/ndb/queries/snippets.py create mode 100644 appengine/standard/ndb/queries/snippets_models.py create mode 100644 appengine/standard/ndb/queries/snippets_test.py create mode 100644 appengine/standard/ndb/schema_update/app.yaml create mode 100644 appengine/standard/ndb/schema_update/main.py create mode 100644 appengine/standard/ndb/schema_update/main_test.py create mode 100644 appengine/standard/ndb/schema_update/models_v1.py create mode 100644 appengine/standard/ndb/schema_update/models_v2.py create mode 100644 appengine/standard/ndb/schema_update/templates/index.html create mode 100644 appengine/standard/ndb/transactions/README.md create mode 100644 appengine/standard/ndb/transactions/app.yaml create mode 100644 appengine/standard/ndb/transactions/appengine_config.py create mode 100644 appengine/standard/ndb/transactions/favicon.ico create mode 100644 appengine/standard/ndb/transactions/main.py create mode 100644 appengine/standard/ndb/transactions/main_test.py create mode 100644 appengine/standard/ndb/transactions/requirements.txt create mode 100644 appengine/standard/remote_api/app.yaml create mode 100644 appengine/standard/remote_api/client.py create mode 100644 appengine/standard/requests/README.md create mode 100644 appengine/standard/requests/app.yaml create mode 100644 appengine/standard/requests/main.py create mode 100644 appengine/standard/requests/main_test.py create mode 100644 appengine/standard/resources/app.yaml create mode 100644 appengine/standard/search/snippets/snippets.py create mode 100644 appengine/standard/search/snippets/snippets_test.py create mode 100644 appengine/standard/sendgrid/README.md create mode 100644 appengine/standard/sendgrid/app.yaml create mode 100644 appengine/standard/sendgrid/appengine_config.py create mode 100644 appengine/standard/sendgrid/main.py create mode 100644 appengine/standard/sendgrid/main_test.py create mode 100644 appengine/standard/sendgrid/requirements.txt create mode 100644 appengine/standard/storage/.gitignore create mode 100644 appengine/standard/storage/api-client/README.md create mode 100644 appengine/standard/storage/api-client/app.yaml create mode 100644 appengine/standard/storage/api-client/appengine_config.py create mode 100644 appengine/standard/storage/api-client/main.py create mode 100644 appengine/standard/storage/api-client/main_test.py create mode 100644 appengine/standard/storage/api-client/requirements.txt create mode 100644 appengine/standard/storage/appengine-client/__init__.py create mode 100644 appengine/standard/storage/appengine-client/app.yaml create mode 100644 appengine/standard/storage/appengine-client/appengine_config.py create mode 100644 appengine/standard/storage/appengine-client/main.py create mode 100644 appengine/standard/storage/appengine-client/main_test.py create mode 100644 appengine/standard/storage/appengine-client/requirements.txt create mode 100644 appengine/standard/taskqueue/counter/README.md create mode 100644 appengine/standard/taskqueue/counter/app.yaml create mode 100644 appengine/standard/taskqueue/counter/application.py create mode 100644 appengine/standard/taskqueue/counter/application_test.py create mode 100644 appengine/standard/taskqueue/counter/queue.yaml create mode 100644 appengine/standard/taskqueue/counter/worker.py create mode 100644 appengine/standard/taskqueue/counter/worker.yaml create mode 100644 appengine/standard/taskqueue/pull-counter/README.md create mode 100644 appengine/standard/taskqueue/pull-counter/app.yaml create mode 100644 appengine/standard/taskqueue/pull-counter/counter.html create mode 100644 appengine/standard/taskqueue/pull-counter/main.py create mode 100644 appengine/standard/taskqueue/pull-counter/pullcounter_test.py create mode 100644 appengine/standard/taskqueue/pull-counter/queue.yaml create mode 100644 appengine/standard/taskqueue/pull-counter/worker.yaml create mode 100644 appengine/standard/urlfetch/README.md create mode 100644 appengine/standard/urlfetch/async/app.yaml create mode 100644 appengine/standard/urlfetch/async/rpc.py create mode 100644 appengine/standard/urlfetch/async/rpc_test.py create mode 100644 appengine/standard/urlfetch/requests/.gitignore create mode 100644 appengine/standard/urlfetch/requests/app.yaml create mode 100644 appengine/standard/urlfetch/requests/appengine_config.py create mode 100644 appengine/standard/urlfetch/requests/main.py create mode 100644 appengine/standard/urlfetch/requests/main_test.py create mode 100644 appengine/standard/urlfetch/requests/requirements.txt create mode 100644 appengine/standard/urlfetch/snippets/app.yaml create mode 100644 appengine/standard/urlfetch/snippets/main.py create mode 100644 appengine/standard/urlfetch/snippets/main_test.py create mode 100644 appengine/standard/users/app.yaml create mode 100644 appengine/standard/users/main.py create mode 100644 appengine/standard/users/main_test.py create mode 100644 appengine/standard/xmpp/README.md create mode 100644 appengine/standard/xmpp/app.yaml create mode 100644 appengine/standard/xmpp/xmpp.py create mode 100644 appengine/standard/xmpp/xmpp_test.py create mode 100644 auth/api-client/requirements.txt create mode 100644 auth/api-client/snippets.py create mode 100644 auth/api-client/snippets_test.py create mode 100644 auth/cloud-client/requirements.txt create mode 100644 auth/cloud-client/snippets.py create mode 100644 auth/cloud-client/snippets_test.py create mode 100644 auth/http-client/requirements.txt create mode 100644 auth/http-client/snippets.py create mode 100644 auth/http-client/snippets_test.py create mode 100644 bigquery/api/README.rst create mode 100644 bigquery/api/README.rst.in create mode 100755 bigquery/api/async_query.py create mode 100644 bigquery/api/async_query_test.py create mode 100755 bigquery/api/export_data_to_cloud_storage.py create mode 100644 bigquery/api/export_data_to_cloud_storage_test.py create mode 100755 bigquery/api/getting_started.py create mode 100644 bigquery/api/getting_started_test.py create mode 100644 bigquery/api/installed_app.py create mode 100644 bigquery/api/installed_app_test.py create mode 100755 bigquery/api/list_datasets_projects.py create mode 100644 bigquery/api/list_datasets_projects_test.py create mode 100755 bigquery/api/load_data_by_post.py create mode 100644 bigquery/api/load_data_by_post_test.py create mode 100755 bigquery/api/load_data_from_csv.py create mode 100644 bigquery/api/load_data_from_csv_test.py create mode 100644 bigquery/api/requirements.txt create mode 100644 bigquery/api/resources/data.csv create mode 100644 bigquery/api/resources/data.json create mode 100644 bigquery/api/resources/schema.json create mode 100644 bigquery/api/resources/streamrows.json create mode 100755 bigquery/api/streaming.py create mode 100644 bigquery/api/streaming_test.py create mode 100755 bigquery/api/sync_query.py create mode 100644 bigquery/api/sync_query_test.py create mode 100644 bigquery/cloud-client/README.rst create mode 100644 bigquery/cloud-client/README.rst.in create mode 100755 bigquery/cloud-client/async_query.py create mode 100644 bigquery/cloud-client/async_query_test.py create mode 100644 bigquery/cloud-client/export_data_to_gcs.py create mode 100644 bigquery/cloud-client/export_data_to_gcs_test.py create mode 100644 bigquery/cloud-client/load_data_from_file.py create mode 100644 bigquery/cloud-client/load_data_from_file_test.py create mode 100644 bigquery/cloud-client/load_data_from_gcs.py create mode 100644 bigquery/cloud-client/load_data_from_gcs_test.py create mode 100644 bigquery/cloud-client/query_params.py create mode 100644 bigquery/cloud-client/query_params_test.py create mode 100644 bigquery/cloud-client/quickstart.py create mode 100644 bigquery/cloud-client/quickstart_test.py create mode 100644 bigquery/cloud-client/requirements.txt create mode 100644 bigquery/cloud-client/resources/data.csv create mode 100644 bigquery/cloud-client/resources/data.json create mode 100644 bigquery/cloud-client/resources/schema.json create mode 100644 bigquery/cloud-client/simple_app.py create mode 100644 bigquery/cloud-client/simple_app_test.py create mode 100644 bigquery/cloud-client/snippets.py create mode 100644 bigquery/cloud-client/snippets_test.py create mode 100644 bigquery/cloud-client/stream_data.py create mode 100644 bigquery/cloud-client/stream_data_test.py create mode 100755 bigquery/cloud-client/sync_query.py create mode 100644 bigquery/cloud-client/sync_query_test.py create mode 100644 bigquery/cloud-client/user_credentials.py create mode 100644 bigquery/cloud-client/user_credentials_test.py create mode 100644 bigquery/dml/.gitignore create mode 100644 bigquery/dml/README.rst create mode 100644 bigquery/dml/README.rst.in create mode 100644 bigquery/dml/insert_sql.py create mode 100644 bigquery/dml/insert_sql_test.py create mode 100755 bigquery/dml/populate_db.py create mode 100644 bigquery/dml/populate_db_test.py create mode 100644 bigquery/dml/requirements.txt create mode 100644 bigquery/dml/resources/insert_sql_test.sql create mode 100644 bigquery/rest/README.rst create mode 100644 bigquery/rest/README.rst.in create mode 100644 bigquery/rest/labels.py create mode 100644 bigquery/rest/labels_test.py create mode 100644 bigquery/rest/requirements.txt create mode 100644 bigtable/hello/README.rst create mode 100644 bigtable/hello/README.rst.in create mode 100644 bigtable/hello/main.py create mode 100644 bigtable/hello/main_test.py create mode 100644 bigtable/hello/requirements.txt create mode 100644 bigtable/hello_happybase/README.rst create mode 100644 bigtable/hello_happybase/README.rst.in create mode 100644 bigtable/hello_happybase/main.py create mode 100644 bigtable/hello_happybase/main_test.py create mode 100644 bigtable/hello_happybase/requirements.txt create mode 100644 bigtable/metricscaler/README.rst create mode 100644 bigtable/metricscaler/README.rst.in create mode 100644 bigtable/metricscaler/metricscaler.py create mode 100644 bigtable/metricscaler/metricscaler_test.py create mode 100644 bigtable/metricscaler/requirements.txt create mode 100644 blog/README.md create mode 100644 blog/introduction_to_data_models_in_cloud_datastore/README.md create mode 100644 blog/introduction_to_data_models_in_cloud_datastore/blog.py create mode 100644 blog/introduction_to_data_models_in_cloud_datastore/blog_test.py create mode 100644 blog/introduction_to_data_models_in_cloud_datastore/requirements.txt create mode 100644 blog/introduction_to_data_models_in_cloud_datastore/wiki.py create mode 100644 blog/introduction_to_data_models_in_cloud_datastore/wiki_test.py create mode 100644 codelabs/flex_and_vision/README.md create mode 100644 codelabs/flex_and_vision/app.yaml create mode 100644 codelabs/flex_and_vision/main.py create mode 100644 codelabs/flex_and_vision/requirements.txt create mode 100644 codelabs/flex_and_vision/templates/homepage.html create mode 100644 compute/README.md create mode 100644 compute/api/README.md create mode 100644 compute/api/create_instance.py create mode 100644 compute/api/create_instance_test.py create mode 100644 compute/api/requirements.txt create mode 100644 compute/api/startup-script.sh create mode 100644 compute/auth/access_token.py create mode 100644 compute/auth/access_token_test.py create mode 100644 compute/auth/application_default.py create mode 100644 compute/auth/application_default_test.py create mode 100644 compute/auth/requirements.txt create mode 100644 compute/autoscaler/demo/frontend.py create mode 100644 compute/autoscaler/demo/frontend_test.py create mode 100644 compute/encryption/generate_wrapped_rsa_key.py create mode 100644 compute/encryption/generate_wrapped_rsa_key_test.py create mode 100644 compute/encryption/requirements.txt create mode 100644 compute/metadata/README.md create mode 100644 compute/metadata/main.py create mode 100644 compute/metadata/main_test.py create mode 100644 compute/metadata/requirements.txt create mode 100644 compute/xmpp_wikibot/README.md create mode 100644 compute/xmpp_wikibot/requirements.txt create mode 100644 compute/xmpp_wikibot/wikibot.py create mode 100644 conftest.py create mode 100644 container_engine/api-client/README.rst create mode 100644 container_engine/api-client/README.rst.in create mode 100644 container_engine/api-client/requirements.txt create mode 100644 container_engine/api-client/snippets.py create mode 100644 container_engine/api-client/snippets_test.py create mode 100644 container_engine/django_tutorial/.dockerignore create mode 100644 container_engine/django_tutorial/.gitignore create mode 100644 container_engine/django_tutorial/Dockerfile create mode 100644 container_engine/django_tutorial/Makefile create mode 100644 container_engine/django_tutorial/README.md create mode 100644 container_engine/django_tutorial/__init__.py create mode 100755 container_engine/django_tutorial/manage.py create mode 100644 container_engine/django_tutorial/mysite/__init__.py create mode 100644 container_engine/django_tutorial/mysite/settings.py create mode 100644 container_engine/django_tutorial/mysite/urls.py create mode 100644 container_engine/django_tutorial/mysite/wsgi.py create mode 100644 container_engine/django_tutorial/polls.yaml create mode 100644 container_engine/django_tutorial/polls/__init__.py create mode 100644 container_engine/django_tutorial/polls/admin.py create mode 100644 container_engine/django_tutorial/polls/apps.py create mode 100644 container_engine/django_tutorial/polls/models.py create mode 100644 container_engine/django_tutorial/polls/tests.py create mode 100644 container_engine/django_tutorial/polls/urls.py create mode 100644 container_engine/django_tutorial/polls/views.py create mode 100644 container_engine/django_tutorial/requirements.txt create mode 100644 dataproc/README.md create mode 100644 dataproc/create_cluster_and_submit_job.py create mode 100644 dataproc/dataproc_e2e_test.py create mode 100644 dataproc/list_clusters.py create mode 100644 dataproc/pyspark_sort.py create mode 100644 dataproc/requirements.txt create mode 100644 datastore/cloud-client/README.rst create mode 100644 datastore/cloud-client/README.rst.in create mode 100644 datastore/cloud-client/index.yaml create mode 100644 datastore/cloud-client/quickstart.py create mode 100644 datastore/cloud-client/quickstart_test.py create mode 100644 datastore/cloud-client/requirements.txt create mode 100644 datastore/cloud-client/snippets.py create mode 100644 datastore/cloud-client/snippets_test.py create mode 100644 datastore/cloud-client/tasks.py create mode 100644 datastore/cloud-client/tasks_test.py create mode 100644 dns/api/README.rst create mode 100644 dns/api/README.rst.in create mode 100644 dns/api/main.py create mode 100644 dns/api/main_test.py create mode 100644 dns/api/requirements.txt create mode 100644 endpoints/bookstore-grpc/Dockerfile create mode 100644 endpoints/bookstore-grpc/README.md create mode 100644 endpoints/bookstore-grpc/api_config.yaml create mode 100644 endpoints/bookstore-grpc/api_config_auth.yaml create mode 100644 endpoints/bookstore-grpc/api_config_http.yaml create mode 100644 endpoints/bookstore-grpc/bookstore.proto create mode 100644 endpoints/bookstore-grpc/bookstore.py create mode 100644 endpoints/bookstore-grpc/bookstore_client.py create mode 100644 endpoints/bookstore-grpc/bookstore_server.py create mode 100644 endpoints/bookstore-grpc/generated_pb2/__init__.py create mode 100644 endpoints/bookstore-grpc/generated_pb2/bookstore_pb2.py create mode 100644 endpoints/bookstore-grpc/http_bookstore.proto create mode 100644 endpoints/bookstore-grpc/jwt_token_gen.py create mode 100644 endpoints/bookstore-grpc/requirements.txt create mode 100644 endpoints/bookstore-grpc/status.py create mode 100644 endpoints/getting-started-grpc/Dockerfile create mode 100644 endpoints/getting-started-grpc/README.md create mode 100644 endpoints/getting-started-grpc/api_config.yaml create mode 100644 endpoints/getting-started-grpc/container-engine.yaml create mode 100644 endpoints/getting-started-grpc/greeter_client.py create mode 100644 endpoints/getting-started-grpc/greeter_server.py create mode 100644 endpoints/getting-started-grpc/helloworld_pb2.py create mode 100644 endpoints/getting-started-grpc/helloworld_pb2_grpc.py create mode 100644 endpoints/getting-started-grpc/protos/helloworld.proto create mode 100644 endpoints/getting-started-grpc/requirements.txt create mode 100644 endpoints/getting-started/Dockerfile.custom create mode 100644 endpoints/getting-started/README.md create mode 100644 endpoints/getting-started/app.yaml create mode 100755 endpoints/getting-started/clients/echo-client.py create mode 100644 endpoints/getting-started/clients/google-id-token-client.py create mode 100644 endpoints/getting-started/clients/google-jwt-client.py create mode 100644 endpoints/getting-started/clients/service_to_service_gae_default/app.yaml create mode 100644 endpoints/getting-started/clients/service_to_service_gae_default/main.py create mode 100644 endpoints/getting-started/clients/service_to_service_google_id_token/app.yaml create mode 100644 endpoints/getting-started/clients/service_to_service_google_id_token/main.py create mode 100644 endpoints/getting-started/clients/service_to_service_non_default/app.yaml create mode 100644 endpoints/getting-started/clients/service_to_service_non_default/appengine_config.py create mode 100644 endpoints/getting-started/clients/service_to_service_non_default/main.py create mode 100644 endpoints/getting-started/clients/service_to_service_non_default/requirements.txt create mode 100644 endpoints/getting-started/container-engine.yaml create mode 100644 endpoints/getting-started/main.py create mode 100644 endpoints/getting-started/main_test.py create mode 100644 endpoints/getting-started/openapi.yaml create mode 100644 endpoints/getting-started/requirements.txt create mode 100644 endpoints/kubernetes/README.md create mode 100644 endpoints/kubernetes/grpc-bookstore-server.yaml create mode 100644 endpoints/kubernetes/grpc-bookstore.yaml create mode 100644 error_reporting/README.rst create mode 100644 error_reporting/README.rst.in create mode 100644 error_reporting/report_exception.py create mode 100644 error_reporting/report_exception_test.py create mode 100644 error_reporting/requirements.txt create mode 100644 favicon.ico create mode 100644 iap/README.md create mode 100644 iap/app_engine_app/app.yaml create mode 100644 iap/app_engine_app/appengine_config.py create mode 100644 iap/app_engine_app/iap_demo.py create mode 100644 iap/app_engine_app/requirements.txt create mode 100644 iap/iap_test.py create mode 100644 iap/make_iap_request.py create mode 100644 iap/requirements.txt create mode 100644 iap/validate_jwt.py create mode 100644 iot/api-client/README.md create mode 100755 iot/api-client/generate_keys.sh create mode 100644 iot/api-client/manager/README.rst create mode 100644 iot/api-client/manager/README.rst.in create mode 100644 iot/api-client/manager/manager.py create mode 100644 iot/api-client/manager/manager_test.py create mode 100644 iot/api-client/manager/requirements.txt create mode 100644 iot/api-client/manager/resources/README.md create mode 100644 iot/api-client/manager/resources/ec_public.pem create mode 100644 iot/api-client/manager/resources/rsa_cert.pem create mode 100644 iot/api-client/mqtt_example/README.md create mode 100644 iot/api-client/mqtt_example/cloudiot_mqtt_example.py create mode 100644 iot/api-client/mqtt_example/requirements.txt create mode 100644 iot/api-client/scripts/README.rst create mode 100644 iot/api-client/scripts/README.rst.in create mode 100644 iot/api-client/scripts/iam.py create mode 100644 iot/api-client/scripts/requirements.txt create mode 100644 kms/api-client/README.rst create mode 100644 kms/api-client/README.rst.in create mode 100644 kms/api-client/quickstart.py create mode 100644 kms/api-client/quickstart_test.py create mode 100644 kms/api-client/requirements.txt create mode 100644 kms/api-client/snippets.py create mode 100644 kms/api-client/snippets_test.py create mode 100644 language/README.md create mode 100644 language/api/README.rst create mode 100644 language/api/README.rst.in create mode 100644 language/api/analyze.py create mode 100644 language/api/analyze_test.py create mode 100644 language/api/requirements.txt create mode 100644 language/cloud-client/v1/README.rst create mode 100644 language/cloud-client/v1/README.rst.in create mode 100644 language/cloud-client/v1/quickstart.py create mode 100644 language/cloud-client/v1/quickstart_test.py create mode 100644 language/cloud-client/v1/requirements.txt create mode 100644 language/cloud-client/v1/resources/text.txt create mode 100644 language/cloud-client/v1/snippets.py create mode 100644 language/cloud-client/v1/snippets_test.py create mode 100644 language/cloud-client/v1beta2/README.rst create mode 100644 language/cloud-client/v1beta2/README.rst.in create mode 100644 language/cloud-client/v1beta2/quickstart.py create mode 100644 language/cloud-client/v1beta2/quickstart_test.py create mode 100644 language/cloud-client/v1beta2/requirements.txt create mode 100644 language/cloud-client/v1beta2/resources/text.txt create mode 100644 language/cloud-client/v1beta2/snippets.py create mode 100644 language/cloud-client/v1beta2/snippets_test.py create mode 100644 language/movie_nl/README.md create mode 100644 language/movie_nl/main.py create mode 100644 language/movie_nl/main_test.py create mode 100644 language/movie_nl/requirements.txt create mode 100644 language/ocr_nl/README.md create mode 100755 language/ocr_nl/main.py create mode 100755 language/ocr_nl/main_test.py create mode 100644 language/ocr_nl/requirements.txt create mode 100644 language/sentiment/README.md create mode 100644 language/sentiment/requirements.txt create mode 100644 language/sentiment/resources/mixed.txt create mode 100644 language/sentiment/resources/neg.txt create mode 100644 language/sentiment/resources/neutral.txt create mode 100644 language/sentiment/resources/pos.txt create mode 100644 language/sentiment/sentiment_analysis.py create mode 100644 language/sentiment/sentiment_analysis_test.py create mode 100644 language/syntax_triples/README.md create mode 100644 language/syntax_triples/main.py create mode 100755 language/syntax_triples/main_test.py create mode 100644 language/syntax_triples/requirements.txt create mode 100644 language/syntax_triples/resources/obama_wikipedia.txt create mode 100644 language/tutorial/README.rst create mode 100644 language/tutorial/README.rst.in create mode 100644 language/tutorial/requirements.txt create mode 100644 language/tutorial/reviews/bladerunner-mixed.txt create mode 100644 language/tutorial/reviews/bladerunner-neg.txt create mode 100644 language/tutorial/reviews/bladerunner-neutral.txt create mode 100644 language/tutorial/reviews/bladerunner-pos.txt create mode 100644 language/tutorial/tutorial.py create mode 100644 language/tutorial/tutorial_test.py create mode 100644 logging/api-client/README.rst create mode 100644 logging/api-client/README.rst.in create mode 100644 logging/api-client/list_logs.py create mode 100644 logging/api-client/list_logs_test.py create mode 100644 logging/api-client/requirements.txt create mode 100644 logging/cloud-client/README.rst create mode 100644 logging/cloud-client/README.rst.in create mode 100644 logging/cloud-client/export.py create mode 100644 logging/cloud-client/export_test.py create mode 100644 logging/cloud-client/quickstart.py create mode 100644 logging/cloud-client/quickstart_test.py create mode 100644 logging/cloud-client/requirements.txt create mode 100644 logging/cloud-client/snippets.py create mode 100644 logging/cloud-client/snippets_test.py create mode 100644 ml_engine/online_prediction/README.md create mode 100644 ml_engine/online_prediction/predict.py create mode 100644 ml_engine/online_prediction/predict_test.py create mode 100644 ml_engine/online_prediction/requirements.txt create mode 100644 ml_engine/online_prediction/resources/census_example_bytes.pb create mode 100644 ml_engine/online_prediction/resources/census_test_data.json create mode 100644 monitoring/api/v2/README.rst create mode 100644 monitoring/api/v2/README.rst.in create mode 100644 monitoring/api/v2/auth.py create mode 100644 monitoring/api/v2/auth_test.py create mode 100644 monitoring/api/v2/labeled_custom_metric.py create mode 100644 monitoring/api/v2/labeled_custom_metric_test.py create mode 100644 monitoring/api/v2/lightweight_custom_metric.py create mode 100644 monitoring/api/v2/lightweight_custom_metric_test.py create mode 100644 monitoring/api/v2/requirements.txt create mode 100644 monitoring/api/v3/api-client/README.rst create mode 100644 monitoring/api/v3/api-client/README.rst.in create mode 100644 monitoring/api/v3/api-client/custom_metric.py create mode 100644 monitoring/api/v3/api-client/custom_metric_test.py create mode 100644 monitoring/api/v3/api-client/list_resources.py create mode 100644 monitoring/api/v3/api-client/list_resources_test.py create mode 100644 monitoring/api/v3/api-client/requirements.txt create mode 100644 monitoring/api/v3/cloud-client/README.rst create mode 100644 monitoring/api/v3/cloud-client/README.rst.in create mode 100644 monitoring/api/v3/cloud-client/quickstart.py create mode 100644 monitoring/api/v3/cloud-client/quickstart_test.py create mode 100644 monitoring/api/v3/cloud-client/requirements.txt create mode 100644 monitoring/api/v3/cloud-client/snippets.py create mode 100644 monitoring/api/v3/cloud-client/snippets_test.py create mode 100644 nox.py create mode 100644 pubsub/cloud-client/README.rst create mode 100644 pubsub/cloud-client/README.rst.in create mode 100644 pubsub/cloud-client/iam.py create mode 100644 pubsub/cloud-client/iam_test.py create mode 100644 pubsub/cloud-client/publisher.py create mode 100644 pubsub/cloud-client/publisher_test.py create mode 100644 pubsub/cloud-client/quickstart.py create mode 100644 pubsub/cloud-client/quickstart_test.py create mode 100644 pubsub/cloud-client/requirements.txt create mode 100644 pubsub/cloud-client/subscriber.py create mode 100644 pubsub/cloud-client/subscriber_test.py create mode 100644 pytest.ini create mode 100755 scripts/README.md create mode 100755 scripts/auto_link_to_docs.py create mode 100755 scripts/decrypt-secrets.sh create mode 100755 scripts/encrypt-secrets.sh create mode 100755 scripts/prepare-testing-project.sh create mode 100644 scripts/readme-gen/readme_gen.py create mode 100644 scripts/readme-gen/templates/README.tmpl.rst create mode 100644 scripts/readme-gen/templates/auth.tmpl.rst create mode 100644 scripts/readme-gen/templates/auth_api_key.tmpl.rst create mode 100644 scripts/readme-gen/templates/install_deps.tmpl.rst create mode 100644 scripts/readme-gen/templates/install_portaudio.tmpl.rst create mode 100644 scripts/resources/docs-links.json create mode 100755 scripts/travis.sh create mode 100644 spanner/cloud-client/README.rst create mode 100644 spanner/cloud-client/README.rst.in create mode 100644 spanner/cloud-client/quickstart.py create mode 100644 spanner/cloud-client/quickstart_test.py create mode 100644 spanner/cloud-client/requirements.txt create mode 100644 spanner/cloud-client/snippets.py create mode 100644 spanner/cloud-client/snippets_test.py create mode 100644 speech/cloud-client/README.rst create mode 100644 speech/cloud-client/README.rst.in create mode 100644 speech/cloud-client/quickstart.py create mode 100644 speech/cloud-client/quickstart_test.py create mode 100644 speech/cloud-client/requirements.txt create mode 100644 speech/cloud-client/resources/audio.raw create mode 100644 speech/cloud-client/resources/audio2.raw create mode 100644 speech/cloud-client/transcribe.py create mode 100644 speech/cloud-client/transcribe_async.py create mode 100644 speech/cloud-client/transcribe_async_test.py create mode 100644 speech/cloud-client/transcribe_streaming.py create mode 100644 speech/cloud-client/transcribe_streaming_test.py create mode 100644 speech/cloud-client/transcribe_test.py create mode 100644 storage/api/README.rst create mode 100644 storage/api/README.rst.in create mode 100644 storage/api/compose_objects.py create mode 100644 storage/api/compose_objects_test.py create mode 100644 storage/api/crud_object.py create mode 100644 storage/api/crud_object_test.py create mode 100644 storage/api/customer_supplied_keys.py create mode 100644 storage/api/customer_supplied_keys_test.py create mode 100644 storage/api/list_objects.py create mode 100644 storage/api/list_objects_test.py create mode 100644 storage/api/requirements.txt create mode 100644 storage/api/resources/file1.txt create mode 100644 storage/api/resources/file2.txt create mode 100644 storage/cloud-client/README.rst create mode 100644 storage/cloud-client/README.rst.in create mode 100644 storage/cloud-client/acl.py create mode 100644 storage/cloud-client/acl_test.py create mode 100644 storage/cloud-client/encryption.py create mode 100644 storage/cloud-client/encryption_test.py create mode 100644 storage/cloud-client/iam.py create mode 100644 storage/cloud-client/iam_test.py create mode 100644 storage/cloud-client/notification_polling.py create mode 100644 storage/cloud-client/notification_polling_test.py create mode 100644 storage/cloud-client/quickstart.py create mode 100644 storage/cloud-client/quickstart_test.py create mode 100644 storage/cloud-client/requirements.txt create mode 100644 storage/cloud-client/snippets.py create mode 100644 storage/cloud-client/snippets_test.py create mode 100644 storage/transfer_service/README.rst create mode 100644 storage/transfer_service/README.rst.in create mode 100644 storage/transfer_service/aws_request.py create mode 100644 storage/transfer_service/create_client.py create mode 100644 storage/transfer_service/nearline_request.py create mode 100644 storage/transfer_service/requirements.txt create mode 100644 storage/transfer_service/transfer_check.py create mode 100644 testing/.gitignore create mode 100644 testing/requirements.txt create mode 100644 testing/secrets.tar.enc create mode 100644 testing/test-env.tmpl.sh create mode 100644 translate/cloud-client/README.rst create mode 100644 translate/cloud-client/README.rst.in create mode 100644 translate/cloud-client/quickstart.py create mode 100644 translate/cloud-client/quickstart_test.py create mode 100644 translate/cloud-client/requirements.txt create mode 100644 translate/cloud-client/snippets.py create mode 100644 translate/cloud-client/snippets_test.py create mode 100644 video/cloud-client/analyze/README.rst create mode 100644 video/cloud-client/analyze/README.rst.in create mode 100644 video/cloud-client/analyze/analyze.py create mode 100644 video/cloud-client/analyze/analyze_test.py create mode 100644 video/cloud-client/analyze/requirements.txt create mode 100644 video/cloud-client/analyze/resources/README.md create mode 100644 video/cloud-client/faces/README.rst create mode 100644 video/cloud-client/faces/README.rst.in create mode 100644 video/cloud-client/faces/faces.py create mode 100644 video/cloud-client/faces/faces_test.py create mode 100644 video/cloud-client/faces/requirements.txt create mode 100644 video/cloud-client/labels/README.rst create mode 100644 video/cloud-client/labels/README.rst.in create mode 100644 video/cloud-client/labels/labels.py create mode 100644 video/cloud-client/labels/labels_test.py create mode 100644 video/cloud-client/labels/requirements.txt create mode 100644 video/cloud-client/quickstart/README.rst create mode 100644 video/cloud-client/quickstart/README.rst.in create mode 100644 video/cloud-client/quickstart/quickstart.py create mode 100644 video/cloud-client/quickstart/quickstart_test.py create mode 100644 video/cloud-client/quickstart/requirements.txt create mode 100644 video/cloud-client/shotchange/README.rst create mode 100644 video/cloud-client/shotchange/README.rst.in create mode 100644 video/cloud-client/shotchange/requirements.txt create mode 100644 video/cloud-client/shotchange/shotchange.py create mode 100644 video/cloud-client/shotchange/shotchange_test.py create mode 100644 vision/api/label/README.rst create mode 100644 vision/api/label/README.rst.in create mode 100755 vision/api/label/label.py create mode 100644 vision/api/label/label_test.py create mode 100644 vision/api/label/requirements.txt create mode 100644 vision/api/label/resources/cat.jpg create mode 100644 vision/api/label/snippets.py create mode 100644 vision/api/label/snippets_test.py create mode 100644 vision/cloud-client/crop_hints/.gitignore create mode 100644 vision/cloud-client/crop_hints/README.rst create mode 100644 vision/cloud-client/crop_hints/README.rst.in create mode 100644 vision/cloud-client/crop_hints/crop_hints.py create mode 100644 vision/cloud-client/crop_hints/crop_hints_test.py create mode 100644 vision/cloud-client/crop_hints/requirements.txt create mode 100644 vision/cloud-client/crop_hints/resources/cropme.jpg create mode 100644 vision/cloud-client/detect/README.rst create mode 100644 vision/cloud-client/detect/README.rst.in create mode 100644 vision/cloud-client/detect/detect.py create mode 100644 vision/cloud-client/detect/detect_test.py create mode 100644 vision/cloud-client/detect/requirements.txt create mode 100644 vision/cloud-client/detect/resources/face_no_surprise.jpg create mode 100644 vision/cloud-client/detect/resources/landmark.jpg create mode 100644 vision/cloud-client/detect/resources/logos.png create mode 100644 vision/cloud-client/detect/resources/text.jpg create mode 100644 vision/cloud-client/detect/resources/wakeupcat.jpg create mode 100644 vision/cloud-client/document_text/.gitignore create mode 100644 vision/cloud-client/document_text/README.rst create mode 100644 vision/cloud-client/document_text/README.rst.in create mode 100644 vision/cloud-client/document_text/doctext.py create mode 100644 vision/cloud-client/document_text/doctext_test.py create mode 100644 vision/cloud-client/document_text/requirements.txt create mode 100644 vision/cloud-client/document_text/resources/text_menu.jpg create mode 100644 vision/cloud-client/face_detection/.gitignore create mode 100644 vision/cloud-client/face_detection/README.rst create mode 100644 vision/cloud-client/face_detection/README.rst.in create mode 100755 vision/cloud-client/face_detection/faces.py create mode 100644 vision/cloud-client/face_detection/faces_test.py create mode 100644 vision/cloud-client/face_detection/requirements.txt create mode 100644 vision/cloud-client/face_detection/resources/face-input.jpg create mode 100644 vision/cloud-client/quickstart/README.rst create mode 100644 vision/cloud-client/quickstart/README.rst.in create mode 100644 vision/cloud-client/quickstart/quickstart.py create mode 100644 vision/cloud-client/quickstart/quickstart_test.py create mode 100644 vision/cloud-client/quickstart/requirements.txt create mode 100644 vision/cloud-client/quickstart/resources/wakeupcat.jpg create mode 100644 vision/cloud-client/web/README.rst create mode 100644 vision/cloud-client/web/README.rst.in create mode 100644 vision/cloud-client/web/requirements.txt create mode 100644 vision/cloud-client/web/web_detect.py create mode 100644 vision/cloud-client/web/web_detect_test.py diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000000..2873cbd6afb --- /dev/null +++ b/.coveragerc @@ -0,0 +1,12 @@ +[run] +omit = + lib/* + env/* + */.nox/* + */conftest.py + */google_appengine/* + +[report] +exclude_lines = + pragma: NO COVER + if __name__ == '__main__': diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000000..176a48ea6e7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +.coveralls.yml +*.pyc +.coverage +.tox +coverage.xml +python-docs-samples.json +service-account.json +client-secrets.json +__pycache__ +*db\.sqlite3 +managed_vms/django_tutorial/static/* +**/migrations/* +lib +testing/resources/test-env.sh +testing/resources/service-account.json +testing/resources/client-secrets.json +secrets.tar +.cache +junit.xml +credentials.dat +.nox diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000000..318d883dff9 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,29 @@ +sudo: false +language: python +services: +- memcached +- mysql +- redis-server +branches: + only: + - master +cache: + directories: + - $HOME/.cache +env: + global: + secure: RPClDmwFZnpBIKtMvlzjzZVam4flvJtSvxFD8mHCQVQ//KqyBbQxl970kqStOK7p0RXkOB3XuFDvVixqyuptoQ8wTdgSBEPAub4DwRpcmCc1exzErHIt9zep3cQhSUuzl8N/tNl3o6GG04NsZTeErORqxfDvk3WbqFa9593G358= +addons: + apt: + sources: + - deadsnakes + packages: + - portaudio19-dev + - python3.5 + - python3.5-dev +install: +- pip install --upgrade pip wheel virtualenv +- pip install --upgrade nox-automation coverage +- pip install --upgrade git+https://github.com/dhermes/ci-diff-helper.git +script: +- ./scripts/travis.sh diff --git a/AUTHORING_GUIDE.md b/AUTHORING_GUIDE.md new file mode 100644 index 00000000000..9fcec2e0cba --- /dev/null +++ b/AUTHORING_GUIDE.md @@ -0,0 +1,481 @@ +# Python Sample Authoring Guide + +We're happy you want to write a Python sample! Like a lot of Pythonistas, we're +opinationed and fussy. This guide intends to be a reference for the format and +style expected of samples that live in +[python-docs-samples](https://github.com/GoogleCloudPlatform/python-docs-samples). + +## Canonical sample + +The [Cloud Storage Sample](https://github.com/GoogleCloudPlatform/python-docs-samples/blob/master/storage/cloud-client/snippets.py) +is a great example of what we expect from samples. It's a great sample to copy +and start from. + +## The basics + +No matter what, all samples must: + +1. Have a license header. +1. Pass lint. +1. Be either a web application or a runnable console application. +1. Have a `requirements.txt` containing all of its third-party dependencies. +1. Work in Python 2.7, 3.5, & 3.6. App Engine Standard is exempt as it + only supports 2.7. Our default version is currently Python 3.5. +1. Have tests. +1. Declare all dependencies in a `requirements.txt`. All requirements must + be pinned. + +## Style & linting + +We follow [pep8](https://www.python.org/dev/peps/pep-0008/) and the +*external* [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html) +we verify this with [flake8](https://pypi.python.org/pypi/flake8). In general: + +1. 4 spaces. +1. `CamelCase` only for classes, `snake_case` elsewhere. +1. `_` for private variables, members, functions, and methods only. Samples + should generally have very few private items. +1. `CAP_WITH_UNDERSCORES` for constants. +1. `mixedCase` is only acceptable when interface with code that doesn't follow + our style guide. +1. 79-character line limit. +1. Imports should be in three sections separated by a blank line - standard + library, third-party, and package-local. Sample will have very few + package-local imports. + +See [Automated tools](#automated-tools) for information on how to run the lint +checker. + +Beyond PEP8, there are several idioms and style nits we prefer. + +1. Use single quotes (`'`) except for docstrings (which use `"""`). +1. Typically import modules over members, for example + `from gcloud import datastore` + instead of `from gcloud.datastore import Client`. Although you should use + your best judgment, for example + `from oauth2client.contrib.flask_util import UserOAuth2` + and `from oauth2client.client import GoogleCredentials` are both totally + fine. +1. Never alias imports unless there is a name collision. +1. Use `.format()` over `%` for string interpolation. +1. Generally put a blank line above control statements that start new indented + blocks, for example: + + ```python + # Good + do_stuff() + + if other_stuff(): + more_stuff() + + # Not so good + do_stuff() + if other_stuff(): + more_stuff() + ``` + + This rule can be relaxed for counter or accumulation variables used in loops. +1. Don't use parenthesis on multiple return values (`return one, two`) or in + destructuring assignment (`one, two = some_function()`). +2. Prefer not to do hanging indents if possible. If you break at the first + logical grouping, it shouldn't be necessary. For example: + + ```python + # Good + do_some_stuff_please( + a_parameter, another_parameter, wow_so_many_parameters, + much_parameter, very_function) + + # Not so good + do_some_stuff_please(a_parameter, another_parameter, wow_so_many_parameters, + much_parameter, very_function) + ``` + + Similarly with strings and other such things: + + ```python + long_string = ( + 'Alice was beginning to get very tired of sitting by her sister on ' + 'the bank, and of having nothing to do: once or twice she had peeped ' + 'into the book her sister was reading, but it had no pictures or ...' + ) + ``` +1. Use descriptive variables names in comprehensions, for example: + + ```python + # Good + blogs = [blog for blog in bob_laws_law_blog] + + # Not so good + blogs = [x for x in bob_laws_law_blog] + ``` + +## The sample format + +In general our sample format follows ideas borrowed from +[Literate Programming](https://en.wikipedia.org/wiki/Literate_programming). +Notably, your sample program should self-contained, readable from top to bottom, +and should be fairly self-documenting. Prefer descriptive names. Use comments +and docstrings only as needed to further explain. Always introduce functions and +variables before they are used. Prefer less indirection. Prefer imperative +programming as it is easier to understand. + +### Shebang + +If, and only if, your sample application is a command-line application then +include a shebang as the first line. Separate the shebang from the rest of +the application with a blank line. The shebang should always be: + +```python +#!/usr/bin/env python +``` + +Don't include shebangs in web applications or test files. + +### License header + +All samples should start with the following (modulo shebang line): + +``` +# Copyright 2017 Google, Inc. +# +# 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. +``` + +### Module-level docstring + +All samples should contain a module-level docstring. For command-line +applications, this docstring will be used as the summary when `-h` is passed. +The docstring should be succinct and should avoid repeating information +available in readmes or documentation. + +Here's a simple docstring for a command-line application with straightforward +usage: + +``` +This application demonstrates how to perform basic operations on blobs +(objects) in a Google Cloud Storage bucket. + +For more information, see the README.md under /storage and the documentation +at https://cloud.google.com/storage/docs. +``` + +Here's a docstring from a command-line application that requires a little +bit more explanation: + +```python +"""This application demonstrates how to upload and download encrypted blobs +(objects) in Google Cloud Storage. + +Use `generate-encryption-key` to generate an example key: + + python encryption.py generate-encryption-key + +Then use the key to upload and download files encrypted with a custom key. + +For more information, see the README.md under /storage and the documentation +at https://cloud.google.com/storage/docs/encryption. +""" +``` + +Finally, here's a docstring from a web application: + +```python +"""Google Cloud Endpoints sample application. + +Demonstrates how to create a simple echo API as well as how to use the +various authentication methods available. + +For more information, see the README.md under /appengine/flexible and the +documentation at https://cloud.google.com/appengine/docs/flexible. +""" +``` + +### Functions & classes + +Very few samples will require authoring classes. Prefer functions whenever +possible. See [this video](https://www.youtube.com/watch?v=o9pEzgHorH0) for +some insight into why classes aren't as necessary as you might think in Python. +Classes also introduce cognitive load. If you do write a class in a sample be +prepared to justify its existence during code review. + +Always prefer descriptive function names even if they are long. +For example `upload_file`, `upload_encrypted_file`, and `list_resource_records`. +Similarly, prefer long and descriptive parameter names. For example +`source_file_name`, `dns_zone_name`, and `base64_encryption_key`. + +Here's an example of a top-level function in a command-line application: + +```python +def list_blobs(bucket_name): + """Lists all the blobs in the bucket.""" + storage_client = storage.Client() + bucket = storage_client.get_bucket(bucket_name) + + blobs = bucket.list_blobs() + + for blob in blobs: + print(blob.name) +``` + +Notice the simple docstring and descriptive argument name (`bucket_name` +implying a string instead of just `bucket` which could imply a class instance). + +This particular function is intended to be the "top of the stack" - the function +executed when the command-line sample is run by the user. As such, notice that +it prints the blobs instead of returning. In general top of the stack +functions in command-line applications should print, but use your best +judgment. + +Here's an example of a more complicated top-level function in a command-line +application: + +```python +def download_encrypted_blob( + bucket_name, source_blob_name, destination_file_name, + base64_encryption_key): + """Downloads a previously-encrypted blob from Google Cloud Storage. + + The encryption key provided must be the same key provided when uploading + the blob. + """ + storage_client = storage.Client() + bucket = storage_client.get_bucket(bucket_name) + blob = bucket.blob(source_blob_name) + + # Encryption key must be an AES256 key represented as a bytestring with + # 32 bytes. Since it's passed in as a base64 encoded string, it needs + # to be decoded. + encryption_key = base64.b64decode(base64_encryption_key) + + blob.download_to_filename( + destination_file_name, encryption_key=encryption_key) + + print('Blob {} downloaded to {}.'.format( + source_blob_name, + destination_file_name)) +``` + +Note the verbose parameter names and the extended description that helps the +user form context. If there were more parameters or if the parameters had +complex context, then it might make sense to expand the docstring to include an +`Args` section such as: + +``` +Args: + bucket_name: The name of the cloud storage bucket. + source_blob_name: The name of the blob in the bucket to download. + destination_file_name: The blob will be downloaded to this path. + base64_encryption_key: A base64-encoded RSA256 encryption key. Must be the + same key used to encrypt the file. +``` + +Generally, however, it's rarely necessary to exhaustively document the +parameters this way. Lean towards unsurprising arguments with descriptive names, +as having to resort to this kind of docstring might be extremely accurate but +it comes at the cost of high redundancy, signal-to-noise ratio, and increased +cognitive load. + +Finally, if absolutely necessary feel free to document the type for the +parameters, for example: + +``` +Args: + credentials (google.oauth2.credentials.Credentials): Credentials authorized + for the current user. +``` + +If documenting primitive types, be sure to note if they have a particular set +of constraints, for example `A base64-encoded string` or `Must be between 0 and +10`. + +### Request handlers + +In general these follow the same rules as top-level functions. +Here's a sample function from a web application: + +```python +@app.route('/pubsub/push', methods=['POST']) +def pubsub_push(): + """Receives push notifications from Cloud Pub/Sub.""" + # Verify the token - if it's not the same token used when creating the + # notification channel then this request did not come from Pub/Sub. + 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 +``` + +Note the name of the function matches the URL route. The docstring is kept +simple because reading the function body reveals how the parameters are +used. + +Use `print` or `logging.info` in request handlers to print useful information +as needed. + +### Argparse section + +For command-line samples, you'll need an argparse section to handle +parsing command-line arguments and executing the sample functions. This +section lives within the `if __name__ == '__main__'` clause: + +```python +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) +``` + +Note the use of `__doc__` (the module-level docstring) as the description for +the parser. This helps you not repeat yourself and gives users useful +information when invoking the program. We also use `RawDescriptionHelpFormatter` +to prevent argparse from re-formatting the docstring. + +Command-line arguments should generally have a 1-to-1 match to function +arguments. For example: + +```python +parser.add_argument('source_file_name') +parser.add_argument('destination_blob_name') +``` + +Again, descriptive names prevent you from having to exhaustively describe +every parameter. + +Some samples demonstrate multiple functions. You should use *subparsers* to +handle this, for example: + +```python +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument('bucket_name', help='Your cloud storage bucket.') + + subparsers = parser.add_subparsers(dest='command') + + subparsers.add_parser('list', help=list_blobs.__doc__) + + upload_parser = subparsers.add_parser('upload', help=upload_blob.__doc__) + upload_parser.add_argument('source_file_name') + upload_parser.add_argument('destination_blob_name') + + args = parser.parse_args() + + if args.command == 'list': + list_blobs(args.bucket_name) + elif args.command == 'upload': + upload_blob( + args.bucket_name, + args.source_file_name, + args.destination_blob_name) +``` + +### Local server + +For web application samples using Flask that don't run on App Engine Standard, +the `if __name__ == '__main__'` clause should handle starting the development +server: + +```python +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) +``` +## Writing tests + +* Use [pytest](https://docs.pytest.org/en/latest/)-style tests and plain + asserts. Don't use `unittest`-style tests or `assertX` mthods. +* All tests in this repository are **system tests**. This means they hit real + services and should use little to no mocking. +* Tests should avoid doing very strict assertions. The exact output format + from an API call can change, but as long as sample still works assertions + should pass. +* Tests will run against Python 2.7 and 3. The only exception is App Engine + standard- these samples are only be tested against Python 2.7. +* Samples that use App Engine Standard should use the App Engine testbed for + system testing. See existing App Engine tests for how to use this. + +## Running tests and automated tools + +### Using nox + +The testing of `python-docs-samples` is managed by +[nox](https://nox.readthedocs.io). Nox allows us to run a variety of tests, +including the linter, Python 2.7, Python 3, App Engine, and automatic README +generation. + +To use nox, install it globally with `pip`: + + $ pip install nox-automation + +Nox automatically discovers all samples in the repository and generates three +types of sessions for *each* sample in this repository: + +1. A test sessions (`gae`, `py27` and `py35`) for running the system tests + against a specific Python version. +2. `lint` sessions for running the style linter . +3. `readmegen` sessions for regenerating READMEs. + +Because nox generates all of these sessions, it's often useful to filter down +by just the sample you're working on. For example, if you just want to see +which sessions are available for storage samples: + + $ nox -k storage -l + * gae(sample='./appengine/standard/storage/api-client') + * gae(sample='./appengine/standard/storage/appengine-client') + * lint(sample='./appengine/flexible/storage') + * lint(sample='./appengine/standard/storage/api-client') + * lint(sample='./appengine/standard/storage/appengine-client') + * lint(sample='./storage/api') + * lint(sample='./storage/cloud-client') + * lint(sample='./storage/transfer_service') + * py27(sample='./appengine/flexible/storage') + * py27(sample='./storage/api') + * py27(sample='./storage/cloud-client') + * py35(sample='./appengine/flexible/storage') + * py35(sample='./storage/api') + * py35(sample='./storage/cloud-client') + * readmegen(sample='./storage/api') + * readmegen(sample='./storage/cloud-client') + * readmegen(sample='./storage/transfer_service') + +Now you can use nox to run a specific session, for example, if you want to lint +the storage cloud-client samples: + + $ nox -s "lint(sample='./storage/cloud-client')" + +### Test environment setup + +Because all the tests here are system tests, you'll need to have a Google +Cloud project with billing enabled. Once you have this configured, you'll +need to set environment variables for the tests to be able to use your project +and its resources. See `testing/test-env.tmpl.sh` for a list of all environment +variables used by all tests. Not every test needs all of these variables. + + + + + + + + + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000000..87fe228c33d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,36 @@ +# How to become a contributor and submit your own code + +## Contributor License Agreements + +We'd love to accept your sample apps and patches! Before we can take them, we +have to jump a couple of legal hurdles. + +Please fill out either the individual or corporate Contributor License +Agreement (CLA). + + * If you are an individual writing original source code and you're sure you + own the intellectual property, then you'll need to sign an [individual CLA] + (https://developers.google.com/open-source/cla/individual). + * If you work for a company that wants to allow you to contribute your work, + then you'll need to sign a [corporate CLA] + (https://developers.google.com/open-source/cla/corporate). + +Follow either of the two links above to access the appropriate CLA and +instructions for how to sign and return it. Once we receive it, we'll +be able to accept your pull requests. + +## Contributing A Patch + +1. Submit an issue describing your proposed change to the repo in question. +1. The repo owner will respond to your issue promptly. +1. If your proposed change is accepted, and you haven't already done so, sign a + Contributor License Agreement (see details above). +1. Fork the desired repo, develop and test your code changes. +1. Ensure that your code adheres to the existing style in the sample to which + you are contributing. +1. Ensure that your code has an appropriate set of unit tests which all pass. +1. Submit a pull request. + +## Authoring, testing, and contributing samples + +See [AUTHORING_GUIDE.md](AUTHORING_GUIDE.md). diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md new file mode 100644 index 00000000000..95b687c0151 --- /dev/null +++ b/ISSUE_TEMPLATE.md @@ -0,0 +1,12 @@ +## In which file did you encounter the issue? + + + +### Did you change the file? If so, how? + + + +## Describe the issue + + diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000000..261eeb9e9f8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/README.md b/README.md new file mode 100644 index 00000000000..935b2b78926 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +## Google Cloud Platform Python Samples + +This repository holds the samples used in the python documentation on [cloud.google.com](https://cloud.google.com). + +[![Build Status](https://travis-ci.org/GoogleCloudPlatform/python-docs-samples.svg)](https://travis-ci.org/GoogleCloudPlatform/python-docs-samples) +[![Coverage Status](https://coveralls.io/repos/github/GoogleCloudPlatform/python-docs-samples/badge.svg?branch=HEAD)](https://coveralls.io/github/GoogleCloudPlatform/python-docs-samples?branch=HEAD) + +For more detailed introduction to a product, check the README.md in the +corresponding folder. + +## Contributing changes + +* See [CONTRIBUTING.md](CONTRIBUTING.md) + +## Licensing + +* See [LICENSE](LICENSE) diff --git a/appengine/flexible/.gitignore b/appengine/flexible/.gitignore new file mode 100644 index 00000000000..11a5d337ae2 --- /dev/null +++ b/appengine/flexible/.gitignore @@ -0,0 +1,54 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ diff --git a/appengine/flexible/README.md b/appengine/flexible/README.md new file mode 100644 index 00000000000..18c2971ed18 --- /dev/null +++ b/appengine/flexible/README.md @@ -0,0 +1,68 @@ +## Google App Engine Flexible Environment Python Samples + +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). + +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, pleaese 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. + + ``` + 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/analytics/README.md b/appengine/flexible/analytics/README.md new file mode 100644 index 00000000000..60cfece907a --- /dev/null +++ b/appengine/flexible/analytics/README.md @@ -0,0 +1,20 @@ +# Google Analytics Measurement Protocol sample for Google App Engine Flexible + +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 new file mode 100644 index 00000000000..16bffdae113 --- /dev/null +++ b/appengine/flexible/analytics/app.yaml @@ -0,0 +1,11 @@ +runtime: python +env: flex +entrypoint: gunicorn -b :$PORT main:app + +runtime_config: + python_version: 3 + +#[START env] +env_variables: + GA_TRACKING_ID: your-tracking-id +#[END env] diff --git a/appengine/flexible/analytics/main.py b/appengine/flexible/analytics/main.py new file mode 100644 index 00000000000..1c9fb9a7168 --- /dev/null +++ b/appengine/flexible/analytics/main.py @@ -0,0 +1,74 @@ +# Copyright 2015 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START app] +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 + } + + response = requests.post( + 'http://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 app] diff --git a/appengine/flexible/analytics/main_test.py b/appengine/flexible/analytics/main_test.py new file mode 100644 index 00000000000..490e43ea967 --- /dev/null +++ b/appengine/flexible/analytics/main_test.py @@ -0,0 +1,47 @@ +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import 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.txt b/appengine/flexible/analytics/requirements.txt new file mode 100644 index 00000000000..b2e539b0cd0 --- /dev/null +++ b/appengine/flexible/analytics/requirements.txt @@ -0,0 +1,3 @@ +Flask==0.12.2 +gunicorn==19.7.1 +requests[security]==2.18.1 diff --git a/appengine/flexible/cloudsql/README.md b/appengine/flexible/cloudsql/README.md new file mode 100644 index 00000000000..76ccabd80c4 --- /dev/null +++ b/appengine/flexible/cloudsql/README.md @@ -0,0 +1,52 @@ +# Python Google Cloud SQL sample for Google App Engine Flexible + +This sample demonstrates how to use [Google Cloud SQL](https://cloud.google.com/sql/) (or any other SQL server) on [Google App Engine Flexible](https://cloud.google.com/appengine). + +## Setup + +Before you can run or deploy the sample, you will need to do the following: + +1. Create a [Second Generation Cloud SQL](https://cloud.google.com/sql/docs/create-instance) instance. You can do this from the [Cloud Console](https://console.developers.google.com) or via the [Cloud SDK](https://cloud.google.com/sdk). To create it via the SDK use the following command: + + $ gcloud sql instances create YOUR_INSTANCE_NAME \ + --activation-policy=ALWAYS \ + --tier=db-n1-standard-1 + +1. Set the root password on your Cloud SQL instance: + + $ gcloud sql instances set-root-password YOUR_INSTANCE_NAME --password YOUR_INSTANCE_ROOT_PASSWORD + +1. Create a [Service Account](https://cloud.google.com/sql/docs/external#createServiceAccount) for your project. You'll use this service account to connect to your Cloud SQL instance locally. + +1. Download the [Cloud SQL Proxy](https://cloud.google.com/sql/docs/sql-proxy). + +1. Run the proxy to allow connecting to your instance from your machine. + + $ cloud_sql_proxy \ + -dir /tmp/cloudsql \ + -instances=YOUR_PROJECT_ID:us-central1:YOUR_INSTANCE_NAME=tcp:3306 \ + -credential_file=PATH_TO_YOUR_SERVICE_ACCOUNT_JSON + +1. Use the MySQL command line tools (or a management tool of your choice) to create a [new user](https://cloud.google.com/sql/docs/create-user) and [database](https://cloud.google.com/sql/docs/create-database) for your application: + + $ mysql -h 127.0.0.1 -u root -p + mysql> create database YOUR_DATABASE; + mysql> create user 'YOUR_USER'@'%' identified by 'PASSWORD'; + mysql> grant all on YOUR_DATABASE.* to 'YOUR_USER'@'%'; + +1. Set the connection string environment variable. This allows the app to connect to your Cloud SQL instance through the proxy: + + export SQLALCHEMY_DATABASE_URI=mysql+pymysql://USER:PASSWORD@127.0.0.1/YOUR_DATABASE + +1. Run ``create_tables.py`` to ensure that the database is properly configured and to create the tables needed for the sample. + +1. Update the connection string in ``app.yaml`` with your configuration values. These values are used when the application is deployed. + +## Running locally + +Refer to the [top-level README](../README.md) for instructions on running and deploying. + +It's recommended to follow the instructions above to run the Cloud SQL proxy. You will need to set the following environment variables via your shell before running the sample: + + $ export SQLALCHEMY_DATABASE_URI=[your connection string] + $ python main.py diff --git a/appengine/flexible/cloudsql/app.yaml b/appengine/flexible/cloudsql/app.yaml new file mode 100644 index 00000000000..377c8a4b171 --- /dev/null +++ b/appengine/flexible/cloudsql/app.yaml @@ -0,0 +1,21 @@ +runtime: python +env: flex +entrypoint: gunicorn -b :$PORT main:app + +runtime_config: + python_version: 3 + +#[START env] +env_variables: + # Replace user, password, database, and instance connection name with the values obtained + # when configuring your Cloud SQL instance. + SQLALCHEMY_DATABASE_URI: >- + mysql+pymysql://USER:PASSWORD@/DATABASE?unix_socket=/cloudsql/INSTANCE_CONNECTION_NAME +#[END env] + +#[START cloudsql_settings] +# Replace project and instance with the values obtained when configuring your +# Cloud SQL instance. +beta_settings: + cloud_sql_instances: INSTANCE_CONNECTION_NAME +#[END cloudsql_settings] diff --git a/appengine/flexible/cloudsql/create_tables.py b/appengine/flexible/cloudsql/create_tables.py new file mode 100755 index 00000000000..2a71c66b014 --- /dev/null +++ b/appengine/flexible/cloudsql/create_tables.py @@ -0,0 +1,25 @@ +#! /usr/bin/env python +# Copyright 2015 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START all] + +from main import db + + +if __name__ == '__main__': + print('Creating all database tables...') + db.create_all() + print('Done!') +# [END all] diff --git a/appengine/flexible/cloudsql/main.py b/appengine/flexible/cloudsql/main.py new file mode 100644 index 00000000000..b64c3f08cde --- /dev/null +++ b/appengine/flexible/cloudsql/main.py @@ -0,0 +1,97 @@ +# Copyright 2015 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import datetime +import logging +import os +import socket + +from flask import Flask, request +from flask_sqlalchemy import SQLAlchemy +import sqlalchemy + + +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] +# Environment variables are defined in app.yaml. +app.config['SQLALCHEMY_DATABASE_URI'] = os.environ['SQLALCHEMY_DATABASE_URI'] +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + +db = SQLAlchemy(app) + + +class Visit(db.Model): + id = db.Column(db.Integer, primary_key=True) + timestamp = db.Column(db.DateTime()) + user_ip = db.Column(db.String(46)) + + def __init__(self, timestamp, user_ip): + self.timestamp = timestamp + self.user_ip = user_ip + + +@app.route('/') +def index(): + 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]) + + visit = Visit( + user_ip=user_ip, + timestamp=datetime.datetime.utcnow() + ) + + db.session.add(visit) + db.session.commit() + + visits = Visit.query.order_by(sqlalchemy.desc(Visit.timestamp)).limit(10) + + results = [ + 'Time: {} Addr: {}'.format(x.timestamp, x.user_ip) + for x in visits] + + output = 'Last 10 visits:\n{}'.format('\n'.join(results)) + + 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/cloudsql/main_test.py b/appengine/flexible/cloudsql/main_test.py new file mode 100644 index 00000000000..49032fc0336 --- /dev/null +++ b/appengine/flexible/cloudsql/main_test.py @@ -0,0 +1,26 @@ +# Copyright 2015 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import main + + +def test_index(): + main.db.create_all() + + 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/cloudsql/requirements.txt b/appengine/flexible/cloudsql/requirements.txt new file mode 100644 index 00000000000..e7a13f9115f --- /dev/null +++ b/appengine/flexible/cloudsql/requirements.txt @@ -0,0 +1,5 @@ +Flask==0.12.2 +Flask-SQLAlchemy==2.2 +gunicorn==19.7.1 +PyMySQL==0.7.11 +psycopg2==2.7.1 diff --git a/appengine/flexible/datastore/README.md b/appengine/flexible/datastore/README.md new file mode 100644 index 00000000000..da3c992ebb6 --- /dev/null +++ b/appengine/flexible/datastore/README.md @@ -0,0 +1,19 @@ +# Python Google Cloud Datastore sample for Google App Engine Flexible Environment + +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 new file mode 100644 index 00000000000..e5ac514e8b6 --- /dev/null +++ b/appengine/flexible/datastore/app.yaml @@ -0,0 +1,6 @@ +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 new file mode 100644 index 00000000000..3130edd53ff --- /dev/null +++ b/appengine/flexible/datastore/main.py @@ -0,0 +1,80 @@ +# Copyright 2015 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +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 example] +@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.utcnow() + }) + + ds.put(entity) + + query = ds.query(kind='visit', order=('-timestamp',)) + + results = [ + 'Time: {timestamp} Addr: {user_ip}'.format(**x) + for x in query.fetch(limit=10)] + + output = 'Last 10 visits:\n{}'.format('\n'.join(results)) + + 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/datastore/main_test.py b/appengine/flexible/datastore/main_test.py new file mode 100644 index 00000000000..069f8a77f25 --- /dev/null +++ b/appengine/flexible/datastore/main_test.py @@ -0,0 +1,24 @@ +# Copyright 2015 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +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/datastore/requirements.txt b/appengine/flexible/datastore/requirements.txt new file mode 100644 index 00000000000..1d05c74e5cb --- /dev/null +++ b/appengine/flexible/datastore/requirements.txt @@ -0,0 +1,3 @@ +Flask==0.12.2 +google-cloud-datastore==1.1.0 +gunicorn==19.7.1 diff --git a/appengine/flexible/disk/app.yaml b/appengine/flexible/disk/app.yaml new file mode 100644 index 00000000000..e5ac514e8b6 --- /dev/null +++ b/appengine/flexible/disk/app.yaml @@ -0,0 +1,6 @@ +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 new file mode 100644 index 00000000000..188eb841bb5 --- /dev/null +++ b/appengine/flexible/disk/main.py @@ -0,0 +1,74 @@ +# Copyright 2015 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +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 new file mode 100644 index 00000000000..069f8a77f25 --- /dev/null +++ b/appengine/flexible/disk/main_test.py @@ -0,0 +1,24 @@ +# Copyright 2015 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +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.txt b/appengine/flexible/disk/requirements.txt new file mode 100644 index 00000000000..bb84e50e90f --- /dev/null +++ b/appengine/flexible/disk/requirements.txt @@ -0,0 +1,2 @@ +Flask==0.12.2 +gunicorn==19.7.1 diff --git a/appengine/flexible/django_cloudsql/README.md b/appengine/flexible/django_cloudsql/README.md new file mode 100644 index 00000000000..5030de82a8b --- /dev/null +++ b/appengine/flexible/django_cloudsql/README.md @@ -0,0 +1,20 @@ +# Getting started with Django on Google Cloud Platform on App Engine Flexible + +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/1.9/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 new file mode 100644 index 00000000000..96c7a992b2d --- /dev/null +++ b/appengine/flexible/django_cloudsql/app.yaml @@ -0,0 +1,11 @@ +# [START runtime] +runtime: python +env: flex +entrypoint: gunicorn -b :$PORT mysite.wsgi + +beta_settings: + cloud_sql_instances: + +runtime_config: + python_version: 3 +# [END runtime] diff --git a/appengine/flexible/django_cloudsql/manage.py b/appengine/flexible/django_cloudsql/manage.py new file mode 100755 index 00000000000..2062ecd2666 --- /dev/null +++ b/appengine/flexible/django_cloudsql/manage.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python +# Copyright 2015 Google Inc. +# +# 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 new file mode 100644 index 00000000000..e69de29bb2d diff --git a/appengine/flexible/django_cloudsql/mysite/settings.py b/appengine/flexible/django_cloudsql/mysite/settings.py new file mode 100644 index 00000000000..b0ebc6d6368 --- /dev/null +++ b/appengine/flexible/django_cloudsql/mysite/settings.py @@ -0,0 +1,125 @@ +# Copyright 2015 Google Inc. +# +# 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. + +# 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 = 'pf-@jxtojga)z+4s*uwbgjrq$aep62-thd0q7f&o77xtpka!_m' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +# 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. +# See https://docs.djangoproject.com/en/1.10/ref/settings/ +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_CLASSES = ( + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'django.middleware.security.SecurityMiddleware', +) + +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 +# https://docs.djangoproject.com/en/1.8/ref/settings/#databases + +# [START dbconfig] +DATABASES = { + 'default': { + # If you are using Cloud SQL for MySQL rather than PostgreSQL, set + # 'ENGINE': 'django.db.backends.mysql' instead of the following. + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': 'polls', + 'USER': '', + 'PASSWORD': '', + } +} +# In the flexible environment, you connect to CloudSQL using a unix socket. +# Locally, you can use the CloudSQL proxy to proxy a localhost connection +# to the instance +DATABASES['default']['HOST'] = '/cloudsql/' +if os.getenv('GAE_INSTANCE'): + pass +else: + DATABASES['default']['HOST'] = '127.0.0.1' +# [END dbconfig] + +# 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/ + +# [START staticurl] +# Fill in your cloud bucket and switch which one of the following 2 lines +# is commented to serve static content from GCS +# STATIC_URL = 'https://storage.googleapis.com//static/' +STATIC_URL = '/static/' +# [END staticurl] + +STATIC_ROOT = 'static/' diff --git a/appengine/flexible/django_cloudsql/mysite/urls.py b/appengine/flexible/django_cloudsql/mysite/urls.py new file mode 100644 index 00000000000..302e8e9e675 --- /dev/null +++ b/appengine/flexible/django_cloudsql/mysite/urls.py @@ -0,0 +1,28 @@ +# Copyright 2015 Google Inc. +# +# 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.conf import settings +from django.conf.urls import include, url +from django.contrib import admin +from django.contrib.staticfiles.urls import staticfiles_urlpatterns + +urlpatterns = [url(r'^', include('polls.urls')), + url(r'^admin/', admin.site.urls)] + + +# This enables static files to be served from the Gunicorn server +# In Production, serve static files from Google Cloud Storage or an alternative +# CDN +if settings.DEBUG: + urlpatterns += staticfiles_urlpatterns() diff --git a/appengine/flexible/django_cloudsql/mysite/wsgi.py b/appengine/flexible/django_cloudsql/mysite/wsgi.py new file mode 100644 index 00000000000..bd3a0d0c2b7 --- /dev/null +++ b/appengine/flexible/django_cloudsql/mysite/wsgi.py @@ -0,0 +1,22 @@ +# Copyright 2015 Google Inc. +# +# 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/polls/__init__.py b/appengine/flexible/django_cloudsql/polls/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/appengine/flexible/django_cloudsql/polls/admin.py b/appengine/flexible/django_cloudsql/polls/admin.py new file mode 100644 index 00000000000..d0fde6f1f6e --- /dev/null +++ b/appengine/flexible/django_cloudsql/polls/admin.py @@ -0,0 +1,19 @@ +# Copyright 2015 Google Inc. +# +# 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 new file mode 100644 index 00000000000..6f98d05c893 --- /dev/null +++ b/appengine/flexible/django_cloudsql/polls/apps.py @@ -0,0 +1,19 @@ +# Copyright 2015 Google Inc. +# +# 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 new file mode 100644 index 00000000000..c38f6e55c06 --- /dev/null +++ b/appengine/flexible/django_cloudsql/polls/models.py @@ -0,0 +1,28 @@ +# Copyright 2015 Google Inc. +# +# 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/tests.py b/appengine/flexible/django_cloudsql/polls/tests.py new file mode 100644 index 00000000000..052a900bd6f --- /dev/null +++ b/appengine/flexible/django_cloudsql/polls/tests.py @@ -0,0 +1,15 @@ +# Copyright 2015 Google Inc. +# +# 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. + +# Create your tests here. diff --git a/appengine/flexible/django_cloudsql/polls/urls.py b/appengine/flexible/django_cloudsql/polls/urls.py new file mode 100644 index 00000000000..5bd8f06f331 --- /dev/null +++ b/appengine/flexible/django_cloudsql/polls/urls.py @@ -0,0 +1,21 @@ +# Copyright 2015 Google Inc. +# +# 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.conf.urls import url + +from . import views + +urlpatterns = [ + url(r'^$', views.index, name='index'), +] diff --git a/appengine/flexible/django_cloudsql/polls/views.py b/appengine/flexible/django_cloudsql/polls/views.py new file mode 100644 index 00000000000..acc5671ba62 --- /dev/null +++ b/appengine/flexible/django_cloudsql/polls/views.py @@ -0,0 +1,19 @@ +# Copyright 2015 Google Inc. +# +# 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.txt b/appengine/flexible/django_cloudsql/requirements.txt new file mode 100644 index 00000000000..cd3dc3e515e --- /dev/null +++ b/appengine/flexible/django_cloudsql/requirements.txt @@ -0,0 +1,5 @@ +Django==1.11.2 +mysqlclient==1.3.10 +wheel==0.29.0 +gunicorn==19.7.1 +psycopg2==2.7.1 diff --git a/appengine/flexible/extending_runtime/.dockerignore b/appengine/flexible/extending_runtime/.dockerignore new file mode 100644 index 00000000000..cc6c24ef97f --- /dev/null +++ b/appengine/flexible/extending_runtime/.dockerignore @@ -0,0 +1,8 @@ +env +*.pyc +__pycache__ +.dockerignore +Dockerfile +.git +.hg +.svn diff --git a/appengine/flexible/extending_runtime/Dockerfile b/appengine/flexible/extending_runtime/Dockerfile new file mode 100644 index 00000000000..d2b2524b48e --- /dev/null +++ b/appengine/flexible/extending_runtime/Dockerfile @@ -0,0 +1,20 @@ +# [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 new file mode 100644 index 00000000000..ce2a124359b --- /dev/null +++ b/appengine/flexible/extending_runtime/app.yaml @@ -0,0 +1,2 @@ +runtime: custom +env: flex diff --git a/appengine/flexible/extending_runtime/main.py b/appengine/flexible/extending_runtime/main.py new file mode 100644 index 00000000000..4cb173f7a68 --- /dev/null +++ b/appengine/flexible/extending_runtime/main.py @@ -0,0 +1,46 @@ +# Copyright 2015 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [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 new file mode 100644 index 00000000000..7c6fe2e1f10 --- /dev/null +++ b/appengine/flexible/extending_runtime/main_test.py @@ -0,0 +1,31 @@ +# Copyright 2015 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +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.txt b/appengine/flexible/extending_runtime/requirements.txt new file mode 100644 index 00000000000..bb84e50e90f --- /dev/null +++ b/appengine/flexible/extending_runtime/requirements.txt @@ -0,0 +1,2 @@ +Flask==0.12.2 +gunicorn==19.7.1 diff --git a/appengine/flexible/hello_world/app.yaml b/appengine/flexible/hello_world/app.yaml new file mode 100644 index 00000000000..e5ac514e8b6 --- /dev/null +++ b/appengine/flexible/hello_world/app.yaml @@ -0,0 +1,6 @@ +runtime: python +env: flex +entrypoint: gunicorn -b :$PORT main:app + +runtime_config: + python_version: 3 diff --git a/appengine/flexible/hello_world/main.py b/appengine/flexible/hello_world/main.py new file mode 100644 index 00000000000..97eb37d83a2 --- /dev/null +++ b/appengine/flexible/hello_world/main.py @@ -0,0 +1,43 @@ +# Copyright 2015 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START app] +import logging + +from flask import Flask + + +app = Flask(__name__) + + +@app.route('/') +def hello(): + """Return a friendly HTTP greeting.""" + return 'Hello World!' + + +@app.errorhandler(500) +def server_error(e): + logging.exception('An error occurred during a request.') + return """ + An internal error occurred:
{}
+ See logs for full stacktrace. + """.format(e), 500 + + +if __name__ == '__main__': + # This is used when running locally. Gunicorn is used to run the + # application on Google App Engine. See entrypoint in app.yaml. + app.run(host='127.0.0.1', port=8080, debug=True) +# [END app] diff --git a/appengine/flexible/hello_world/main_test.py b/appengine/flexible/hello_world/main_test.py new file mode 100644 index 00000000000..4e230185244 --- /dev/null +++ b/appengine/flexible/hello_world/main_test.py @@ -0,0 +1,24 @@ +# Copyright 2015 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import main + + +def test_index(): + 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/hello_world/requirements.txt b/appengine/flexible/hello_world/requirements.txt new file mode 100644 index 00000000000..bb84e50e90f --- /dev/null +++ b/appengine/flexible/hello_world/requirements.txt @@ -0,0 +1,2 @@ +Flask==0.12.2 +gunicorn==19.7.1 diff --git a/appengine/flexible/hello_world_django/.gitignore b/appengine/flexible/hello_world_django/.gitignore new file mode 100644 index 00000000000..49ef2557b16 --- /dev/null +++ b/appengine/flexible/hello_world_django/.gitignore @@ -0,0 +1 @@ +db.sqlite3 diff --git a/appengine/flexible/hello_world_django/README.md b/appengine/flexible/hello_world_django/README.md new file mode 100644 index 00000000000..2e2891210db --- /dev/null +++ b/appengine/flexible/hello_world_django/README.md @@ -0,0 +1,58 @@ +# Django sample for Google App Engine Flexible Environment + +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 emphemmeral, +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 straightfoward + 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 new file mode 100644 index 00000000000..ea714d595d5 --- /dev/null +++ b/appengine/flexible/hello_world_django/app.yaml @@ -0,0 +1,6 @@ +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 new file mode 100644 index 00000000000..e69de29bb2d diff --git a/appengine/flexible/hello_world_django/helloworld/views.py b/appengine/flexible/hello_world_django/helloworld/views.py new file mode 100644 index 00000000000..e6b9069a796 --- /dev/null +++ b/appengine/flexible/hello_world_django/helloworld/views.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +# Copyright 2015 Google Inc. +# +# 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 new file mode 100755 index 00000000000..0eacf42df3e --- /dev/null +++ b/appengine/flexible/hello_world_django/manage.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +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 new file mode 100644 index 00000000000..e69de29bb2d diff --git a/appengine/flexible/hello_world_django/project_name/settings.py b/appengine/flexible/hello_world_django/project_name/settings.py new file mode 100644 index 00000000000..e96651fa8f7 --- /dev/null +++ b/appengine/flexible/hello_world_django/project_name/settings.py @@ -0,0 +1,117 @@ +# Copyright 2015 Google Inc. +# +# 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_CLASSES = ( + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'django.middleware.security.SecurityMiddleware', +) + +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 new file mode 100644 index 00000000000..883aa60e939 --- /dev/null +++ b/appengine/flexible/hello_world_django/project_name/urls.py @@ -0,0 +1,39 @@ +# Copyright 2015 Google Inc. +# +# 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 new file mode 100644 index 00000000000..242a7ebfb1e --- /dev/null +++ b/appengine/flexible/hello_world_django/project_name/wsgi.py @@ -0,0 +1,16 @@ +""" +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.txt b/appengine/flexible/hello_world_django/requirements.txt new file mode 100644 index 00000000000..dde5a9f5a97 --- /dev/null +++ b/appengine/flexible/hello_world_django/requirements.txt @@ -0,0 +1,2 @@ +Django==1.11.2 +gunicorn==19.7.1 diff --git a/appengine/flexible/mailgun/README.md b/appengine/flexible/mailgun/README.md new file mode 100644 index 00000000000..92703fa1859 --- /dev/null +++ b/appengine/flexible/mailgun/README.md @@ -0,0 +1,24 @@ +# Python Mailgun email sample for Google App Engine Flexible Environment + +This sample demonstrates how to use [Mailgun](https://www.mailgun.com) on [Google App Engine Flexible Environment](https://cloud.google.com/appengine). + +For more information about Mail, see their [documentation](https://documentation.mailgun.com/). + +## Setup + +Before you can run or deploy the sample, you will need to do the following: + +1. [Create a Mailgun Account](http://www.mailgun.com/google). As of September 2015, Google users start with 30,000 free emails per month. + +2. Configure your Mailgun 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 and send emails from your local machine. You +will need to set environment variables before starting your application: + + $ export MAILGUN_API_KEY=[your-mailgun-api-key] + $ export MAILGUN_DOMAIN_NAME=[your-mailgun-domain-name] + $ python main.py diff --git a/appengine/flexible/mailgun/app.yaml b/appengine/flexible/mailgun/app.yaml new file mode 100644 index 00000000000..a3e24700457 --- /dev/null +++ b/appengine/flexible/mailgun/app.yaml @@ -0,0 +1,12 @@ +runtime: python +env: flex +entrypoint: gunicorn -b :$PORT main:app + +runtime_config: + python_version: 3 + +# [START env_variables] +env_variables: + MAILGUN_DOMAIN_NAME: your-mailgun-domain-name + MAILGUN_API_KEY: your-mailgun-api-key +# [END env_variables] diff --git a/appengine/flexible/mailgun/example-attachment.txt b/appengine/flexible/mailgun/example-attachment.txt new file mode 100644 index 00000000000..96feda20c1c --- /dev/null +++ b/appengine/flexible/mailgun/example-attachment.txt @@ -0,0 +1 @@ +This is a sample attachment. diff --git a/appengine/flexible/mailgun/main.py b/appengine/flexible/mailgun/main.py new file mode 100644 index 00000000000..d8872e742da --- /dev/null +++ b/appengine/flexible/mailgun/main.py @@ -0,0 +1,95 @@ +# Copyright 2015 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START app] +import logging +import os + +from flask import Flask, render_template, request +import requests + +# [START config] +MAILGUN_DOMAIN_NAME = os.environ['MAILGUN_DOMAIN_NAME'] +MAILGUN_API_KEY = os.environ['MAILGUN_API_KEY'] +# [END config] + +app = Flask(__name__) + + +# [START simple_message] +def send_simple_message(to): + url = 'https://api.mailgun.net/v3/{}/messages'.format(MAILGUN_DOMAIN_NAME) + auth = ('api', MAILGUN_API_KEY) + data = { + 'from': 'Mailgun User '.format(MAILGUN_DOMAIN_NAME), + 'to': to, + 'subject': 'Simple Mailgun Example', + 'text': 'Plaintext content', + } + + response = requests.post(url, auth=auth, data=data) + response.raise_for_status() +# [END simple_message] + + +# [START complex_message] +def send_complex_message(to): + url = 'https://api.mailgun.net/v3/{}/messages'.format(MAILGUN_DOMAIN_NAME) + auth = ('api', MAILGUN_API_KEY) + data = { + 'from': 'Mailgun User '.format(MAILGUN_DOMAIN_NAME), + 'to': to, + 'subject': 'Complex Mailgun Example', + 'text': 'Plaintext content', + 'html': 'HTML content' + } + files = [("attachment", open("example-attachment.txt"))] + + response = requests.post(url, auth=auth, data=data, files=files) + response.raise_for_status() +# [END complex_message] + + +@app.route('/') +def index(): + return render_template('index.html') + + +@app.route('/send/email', methods=['POST']) +def send_email(): + action = request.form.get('submit') + to = request.form.get('to') + + if action == 'Send simple email': + send_simple_message(to) + else: + send_complex_message(to) + + return 'Email sent.' + + +@app.errorhandler(500) +def server_error(e): + logging.exception('An error occurred during a request.') + return """ + An internal error occurred:
{}
+ See logs for full stacktrace. + """.format(e), 500 + + +if __name__ == '__main__': + # This is used when running locally. Gunicorn is used to run the + # application on Google App Engine. See entrypoint in app.yaml. + app.run(host='127.0.0.1', port=8080, debug=True) +# [END app] diff --git a/appengine/flexible/mailgun/main_test.py b/appengine/flexible/mailgun/main_test.py new file mode 100644 index 00000000000..b92bda25913 --- /dev/null +++ b/appengine/flexible/mailgun/main_test.py @@ -0,0 +1,80 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +import pytest +import requests +import responses + + +@pytest.fixture +def app(monkeypatch): + monkeypatch.setenv('MAILGUN_DOMAIN_NAME', 'example.com') + monkeypatch.setenv('MAILGUN_API_KEY', 'apikey') + + import main + + main.app.testing = True + return main.app.test_client() + + +def test_index(app): + r = app.get('/') + assert r.status_code == 200 + + +@responses.activate +def test_send_error(app): + responses.add( + responses.POST, + 'https://api.mailgun.net/v3/example.com/messages', + body='Test error', + status=500) + + with pytest.raises(requests.exceptions.HTTPError): + app.post('/send/email', data={ + 'recipient': 'user@example.com', + 'submit': 'Send simple email'}) + + +@responses.activate +def test_send_simple(app): + responses.add( + responses.POST, + 'https://api.mailgun.net/v3/example.com/messages', + body='') + + response = app.post('/send/email', data={ + 'recipient': 'user@example.com', + 'submit': 'Send simple email'}) + assert response.status_code == 200 + assert len(responses.calls) == 1 + + +@responses.activate +def test_send_complex(app, monkeypatch): + import main + monkeypatch.chdir(os.path.dirname(main.__file__)) + + responses.add( + responses.POST, + 'https://api.mailgun.net/v3/example.com/messages', + body='') + + response = app.post('/send/email', data={ + 'recipient': 'user@example.com', + 'submit': 'Send complex email'}) + assert response.status_code == 200 + assert len(responses.calls) == 1 diff --git a/appengine/flexible/mailgun/requirements.txt b/appengine/flexible/mailgun/requirements.txt new file mode 100644 index 00000000000..b2e539b0cd0 --- /dev/null +++ b/appengine/flexible/mailgun/requirements.txt @@ -0,0 +1,3 @@ +Flask==0.12.2 +gunicorn==19.7.1 +requests[security]==2.18.1 diff --git a/appengine/flexible/mailgun/templates/index.html b/appengine/flexible/mailgun/templates/index.html new file mode 100644 index 00000000000..1aa0f319050 --- /dev/null +++ b/appengine/flexible/mailgun/templates/index.html @@ -0,0 +1,30 @@ +{# +# Copyright 2015 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#} + + + + Mailgun on Google App Engine Flexible Environment + + + +
+ + + +
+ + + diff --git a/appengine/flexible/mailjet/README.md b/appengine/flexible/mailjet/README.md new file mode 100644 index 00000000000..0fb5d38e0b4 --- /dev/null +++ b/appengine/flexible/mailjet/README.md @@ -0,0 +1,23 @@ +# Python Mailjet email sample for Google App Engine Flexible + +This sample demonstrates how to use [Mailjet](https://www.mailgun.com) on [Google App Engine Flexible](https://cloud.google.com/appengine/docs/flexible/). + +## Setup + +Before you can run or deploy the sample, you will need to do the following: + +1. [Create a Mailjet Account](http://www.mailjet.com/google). + +2. Configure your Mailjet 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 and send emails from your local machine. You +will need to set environment variables before starting your application: + + $ export MAILGUN_API_KEY=[your-mailgun-api-key] + $ export MAILGUN_API_SECRET=[your-mailgun-secret] + $ export MAILGUN_SENDER=[your-sender-address] + $ python main.py diff --git a/appengine/flexible/mailjet/app.yaml b/appengine/flexible/mailjet/app.yaml new file mode 100644 index 00000000000..5c34e24d0ac --- /dev/null +++ b/appengine/flexible/mailjet/app.yaml @@ -0,0 +1,13 @@ +runtime: python +env: flex +entrypoint: gunicorn -b :$PORT main:app + +runtime_config: + python_version: 3 + +# [START env_variables] +env_variables: + MAILJET_API_KEY: your-mailjet-api-key + MAILJET_API_SECRET: your-mailjet-api-secret + MAILJET_SENDER: your-mailjet-sender-address +# [END env_variables] diff --git a/appengine/flexible/mailjet/main.py b/appengine/flexible/mailjet/main.py new file mode 100644 index 00000000000..c3ea95a724a --- /dev/null +++ b/appengine/flexible/mailjet/main.py @@ -0,0 +1,78 @@ +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START app] +import logging +import os + +from flask import Flask, render_template, request +# [START config] +import mailjet_rest + +MAILJET_API_KEY = os.environ['MAILJET_API_KEY'] +MAILJET_API_SECRET = os.environ['MAILJET_API_SECRET'] +MAILJET_SENDER = os.environ['MAILJET_SENDER'] +# [END config] + +app = Flask(__name__) + + +# [START send_message] +def send_message(to): + client = mailjet_rest.Client( + auth=(MAILJET_API_KEY, MAILJET_API_SECRET)) + + data = { + 'FromEmail': MAILJET_SENDER, + 'FromName': 'App Engine Flex Mailjet Sample', + 'Subject': 'Example email.', + 'Text-part': 'This is an example email.', + 'Html-part': 'This is an example email.', + 'Recipients': [{'Email': to}] + } + + result = client.send.create(data=data) + + return result.json() +# [END send_message] + + +@app.route('/') +def index(): + return render_template('index.html') + + +@app.route('/send/email', methods=['POST']) +def send_email(): + to = request.form.get('to') + + result = send_message(to) + + return 'Email sent, response:
{}
'.format(result) + + +@app.errorhandler(500) +def server_error(e): + logging.exception('An error occurred during a request.') + return """ + An internal error occurred:
{}
+ See logs for full stacktrace. + """.format(e), 500 + + +if __name__ == '__main__': + # This is used when running locally. Gunicorn is used to run the + # application on Google App Engine. See entrypoint in app.yaml. + app.run(host='127.0.0.1', port=8080, debug=True) +# [END app] diff --git a/appengine/flexible/mailjet/main_test.py b/appengine/flexible/mailjet/main_test.py new file mode 100644 index 00000000000..c910f48c544 --- /dev/null +++ b/appengine/flexible/mailjet/main_test.py @@ -0,0 +1,53 @@ +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import re + +import pytest +import responses + + +@pytest.fixture +def app(monkeypatch): + monkeypatch.setenv('MAILJET_API_KEY', 'apikey') + monkeypatch.setenv('MAILJET_API_SECRET', 'apisecret') + monkeypatch.setenv('MAILJET_SENDER', 'sender') + + import main + + main.app.testing = True + return main.app.test_client() + + +def test_index(app): + r = app.get('/') + assert r.status_code == 200 + + +@responses.activate +def test_send_email(app): + responses.add( + responses.POST, + re.compile(r'.*'), + body='{"test": "message"}', + content_type='application/json') + + r = app.post('/send/email', data={'to': 'user@example.com'}) + + assert r.status_code == 200 + assert 'test' in r.data.decode('utf-8') + + assert len(responses.calls) == 1 + request_body = responses.calls[0].request.body + assert 'user@example.com' in request_body diff --git a/appengine/flexible/mailjet/requirements.txt b/appengine/flexible/mailjet/requirements.txt new file mode 100644 index 00000000000..3f8c3d7e14a --- /dev/null +++ b/appengine/flexible/mailjet/requirements.txt @@ -0,0 +1,4 @@ +Flask==0.12.2 +gunicorn==19.7.1 +requests[security]==2.18.1 +mailjet-rest==1.3.0 diff --git a/appengine/flexible/mailjet/templates/index.html b/appengine/flexible/mailjet/templates/index.html new file mode 100644 index 00000000000..19993c3547d --- /dev/null +++ b/appengine/flexible/mailjet/templates/index.html @@ -0,0 +1,29 @@ +{# +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#} + + + + Mailjet on Google App Engine Flexible + + + +
+ + +
+ + + diff --git a/appengine/flexible/memcache/README.md b/appengine/flexible/memcache/README.md new file mode 100644 index 00000000000..75caf3590be --- /dev/null +++ b/appengine/flexible/memcache/README.md @@ -0,0 +1,44 @@ +## Note + +This sample demonstrates connecting to existing Memcache servers, or the +built-in Memcache server. + +A managed option for Memcache is RedisLabs: + +https://cloud.google.com/appengine/docs/flexible/python/using-redislabs-memcache + +You can install and manage a Memcache server on Google Compute Engine. One way +to do so is to use a Bitnami click-to-deploy: + +https://bitnami.com/stack/memcached/cloud/google + +Built-in Memcache for Flexible environments is currently in a whitelist-only alpha. To have your project whitelisted, +see the signup form here: + +https://cloud.google.com/appengine/docs/flexible/python/upgrading#memcache_service + +## 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 + +Start your application: + + $ python main.py + +## Deploying on App Engine + +Deploy using `gcloud`: + + gcloud app deploy + +You can now access the application at `https://your-app-id.appspot.com`. diff --git a/appengine/flexible/memcache/app.yaml b/appengine/flexible/memcache/app.yaml new file mode 100644 index 00000000000..1135714bca2 --- /dev/null +++ b/appengine/flexible/memcache/app.yaml @@ -0,0 +1,16 @@ +runtime: python +env: flex +entrypoint: gunicorn -b :$PORT main:app + +runtime_config: + python_version: 3 + +# [START env_variables] +env_variables: + MEMCACHE_SERVER: your-memcache-server + # If you are using a third-party or self-hosted Memcached server with SASL + # authentiation enabled, uncomment and fill in these values with your + # username and password. + # MEMCACHE_USERNAME: your-memcache-username + # MEMCACHE_PASSWORD: your-memcache-password +# [END env_variables] diff --git a/appengine/flexible/memcache/main.py b/appengine/flexible/memcache/main.py new file mode 100644 index 00000000000..5109aab19ec --- /dev/null +++ b/appengine/flexible/memcache/main.py @@ -0,0 +1,62 @@ +# Copyright 2015 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import os + +from flask import Flask +import pylibmc + +app = Flask(__name__) + + +# [START client] +# Environment variables are defined in app.yaml. +MEMCACHE_SERVER = os.environ.get('MEMCACHE_SERVER', 'localhost:11211') +MEMCACHE_USERNAME = os.environ.get('MEMCACHE_USERNAME') +MEMCACHE_PASSWORD = os.environ.get('MEMCACHE_PASSWORD') + +memcache_client = pylibmc.Client( + [MEMCACHE_SERVER], binary=True, + username=MEMCACHE_USERNAME, password=MEMCACHE_PASSWORD) +# [END client] + + +# [START example] +@app.route('/') +def index(): + + # Set initial value if necessary + if not memcache_client.get('counter'): + memcache_client.set('counter', 0) + + value = memcache_client.incr('counter', 1) + + return 'Value is {}'.format(value) +# [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/memcache/main_test.py b/appengine/flexible/memcache/main_test.py new file mode 100644 index 00000000000..f78f53ee294 --- /dev/null +++ b/appengine/flexible/memcache/main_test.py @@ -0,0 +1,41 @@ +# Copyright 2015 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +try: + import main +except ImportError: + main = None + + +@pytest.mark.skipif( + not main, + reason='pylibmc not installed.') +def test_index(): + try: + main.memcache_client.set('counter', 0) + except Exception: + pytest.skip('Memcache is unavailable.') + + main.app.testing = True + client = main.app.test_client() + + r = client.get('/') + assert r.status_code == 200 + assert '1' in r.data.decode('utf-8') + + r = client.get('/') + assert r.status_code == 200 + assert '2' in r.data.decode('utf-8') diff --git a/appengine/flexible/memcache/requirements.txt b/appengine/flexible/memcache/requirements.txt new file mode 100644 index 00000000000..e8c4cf77726 --- /dev/null +++ b/appengine/flexible/memcache/requirements.txt @@ -0,0 +1,3 @@ +Flask==0.12.2 +gunicorn==19.7.1 +pylibmc==1.5.2 diff --git a/appengine/flexible/metadata/app.yaml b/appengine/flexible/metadata/app.yaml new file mode 100644 index 00000000000..e5ac514e8b6 --- /dev/null +++ b/appengine/flexible/metadata/app.yaml @@ -0,0 +1,6 @@ +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 new file mode 100644 index 00000000000..7c861dce192 --- /dev/null +++ b/appengine/flexible/metadata/main.py @@ -0,0 +1,70 @@ +# Copyright 2015 Google Inc. +# +# 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 + +from flask import Flask +import requests + + +logging.basicConfig(level=logging.INFO) +app = Flask(__name__) + + +# [START 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 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) +# [END 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/metadata/requirements.txt b/appengine/flexible/metadata/requirements.txt new file mode 100644 index 00000000000..b2e539b0cd0 --- /dev/null +++ b/appengine/flexible/metadata/requirements.txt @@ -0,0 +1,3 @@ +Flask==0.12.2 +gunicorn==19.7.1 +requests[security]==2.18.1 diff --git a/appengine/flexible/multiple_services/README.md b/appengine/flexible/multiple_services/README.md new file mode 100644 index 00000000000..705087f9d76 --- /dev/null +++ b/appengine/flexible/multiple_services/README.md @@ -0,0 +1,62 @@ +# Python Google Cloud Microservices Example - API Gateway + +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/app.yaml static/app.yaml +``` diff --git a/appengine/flexible/multiple_services/gateway-service/app.yaml b/appengine/flexible/multiple_services/gateway-service/app.yaml new file mode 100644 index 00000000000..d544520a684 --- /dev/null +++ b/appengine/flexible/multiple_services/gateway-service/app.yaml @@ -0,0 +1,10 @@ +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 new file mode 100644 index 00000000000..2a233131fe5 --- /dev/null +++ b/appengine/flexible/multiple_services/gateway-service/main.py @@ -0,0 +1,58 @@ +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +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.txt b/appengine/flexible/multiple_services/gateway-service/requirements.txt new file mode 100644 index 00000000000..8a8b8c0da3e --- /dev/null +++ b/appengine/flexible/multiple_services/gateway-service/requirements.txt @@ -0,0 +1,3 @@ +Flask==0.12.2 +gunicorn==19.7.1 +requests==2.18.1 diff --git a/appengine/flexible/multiple_services/gateway-service/services_config.py b/appengine/flexible/multiple_services/gateway-service/services_config.py new file mode 100644 index 00000000000..1c9871cf333 --- /dev/null +++ b/appengine/flexible/multiple_services/gateway-service/services_config.py @@ -0,0 +1,57 @@ +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import 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.environ.get('GAE_LONG_APP_ID') + 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 new file mode 100644 index 00000000000..db3f8b4274c --- /dev/null +++ b/appengine/flexible/multiple_services/static-service/app.yaml @@ -0,0 +1,10 @@ +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 new file mode 100644 index 00000000000..e5ac2be3763 --- /dev/null +++ b/appengine/flexible/multiple_services/static-service/main.py @@ -0,0 +1,46 @@ +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +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 path.split('.')[1] == 'css': + mimetype = 'text/css' + if 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.txt b/appengine/flexible/multiple_services/static-service/requirements.txt new file mode 100644 index 00000000000..8a8b8c0da3e --- /dev/null +++ b/appengine/flexible/multiple_services/static-service/requirements.txt @@ -0,0 +1,3 @@ +Flask==0.12.2 +gunicorn==19.7.1 +requests==2.18.1 diff --git a/appengine/flexible/multiple_services/static-service/static/index.html b/appengine/flexible/multiple_services/static-service/static/index.html new file mode 100644 index 00000000000..b56df2a8862 --- /dev/null +++ b/appengine/flexible/multiple_services/static-service/static/index.html @@ -0,0 +1,32 @@ + + + + + + + + + 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..ef8cdfd5cba --- /dev/null +++ b/appengine/flexible/multiple_services/static-service/static/index.js @@ -0,0 +1,36 @@ +// Copyright 2016 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +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..adc68fa6a4d --- /dev/null +++ b/appengine/flexible/multiple_services/static-service/static/style.css @@ -0,0 +1,3 @@ +h1 { + color: red; +} diff --git a/appengine/flexible/numpy/app.yaml b/appengine/flexible/numpy/app.yaml new file mode 100644 index 00000000000..e5ac514e8b6 --- /dev/null +++ b/appengine/flexible/numpy/app.yaml @@ -0,0 +1,6 @@ +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..77f04de8a2c --- /dev/null +++ b/appengine/flexible/numpy/main.py @@ -0,0 +1,50 @@ +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from flask import Flask +import numpy as np + +app = Flask(__name__) + + +# [START numpy] +@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 +# [END numpy] + + +@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..c313fd6b582 --- /dev/null +++ b/appengine/flexible/numpy/main_test.py @@ -0,0 +1,24 @@ +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import 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.txt b/appengine/flexible/numpy/requirements.txt new file mode 100644 index 00000000000..93db13d0de5 --- /dev/null +++ b/appengine/flexible/numpy/requirements.txt @@ -0,0 +1,3 @@ +Flask==0.12.2 +gunicorn==19.7.1 +numpy==1.13.0 diff --git a/appengine/flexible/pubsub/README.md b/appengine/flexible/pubsub/README.md new file mode 100644 index 00000000000..6f44362739c --- /dev/null +++ b/appengine/flexible/pubsub/README.md @@ -0,0 +1,71 @@ +# Python Google Cloud Pub/Sub sample for Google App Engine Flexible Environment + +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 alpha pubsub topics create [your-topic-name] + $ gcloud alpha 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 new file mode 100644 index 00000000000..9cc0e98de57 --- /dev/null +++ b/appengine/flexible/pubsub/app.yaml @@ -0,0 +1,14 @@ +runtime: python +env: flex +entrypoint: gunicorn -b :$PORT main:app + +runtime_config: + python_version: 3 + +#[START env] +env_variables: + PUBSUB_TOPIC: your-topic + # This token is used to verify that requests originate from your + # application. It can be any sufficiently random string. + PUBSUB_VERIFICATION_TOKEN: 1234abc +#[END env] diff --git a/appengine/flexible/pubsub/main.py b/appengine/flexible/pubsub/main.py new file mode 100644 index 00000000000..433a9d4aaee --- /dev/null +++ b/appengine/flexible/pubsub/main.py @@ -0,0 +1,85 @@ +# Copyright 2015 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START app] +import base64 +import json +import logging +import os + +from flask import current_app, Flask, render_template, request +from google.cloud import pubsub + + +app = Flask(__name__) + +# Configure the following environment variables via app.yaml +# This is used in the push request handler to veirfy that the request came from +# pubsub and originated from a trusted source. +app.config['PUBSUB_VERIFICATION_TOKEN'] = \ + os.environ['PUBSUB_VERIFICATION_TOKEN'] +app.config['PUBSUB_TOPIC'] = os.environ['PUBSUB_TOPIC'] + + +# Global list to storage messages received by this instance. +MESSAGES = [] + + +# [START index] +@app.route('/', methods=['GET', 'POST']) +def index(): + if request.method == 'GET': + return render_template('index.html', messages=MESSAGES) + + ps = pubsub.Client() + topic = ps.topic(current_app.config['PUBSUB_TOPIC']) + + topic.publish( + request.form.get('payload', 'Example payload').encode('utf-8')) + + return 'OK', 200 +# [END index] + + +# [START 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 push] + + +@app.errorhandler(500) +def server_error(e): + logging.exception('An error occurred during a request.') + return """ + An internal error occurred:
    {}
    + See logs for full stacktrace. + """.format(e), 500 + + +if __name__ == '__main__': + # This is used when running locally. Gunicorn is used to run the + # application on Google App Engine. See entrypoint in app.yaml. + app.run(host='127.0.0.1', port=8080, debug=True) +# [END app] diff --git a/appengine/flexible/pubsub/main_test.py b/appengine/flexible/pubsub/main_test.py new file mode 100644 index 00000000000..c2911415275 --- /dev/null +++ b/appengine/flexible/pubsub/main_test.py @@ -0,0 +1,69 @@ +# Copyright 2015 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +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.txt b/appengine/flexible/pubsub/requirements.txt new file mode 100644 index 00000000000..a62bb0a21d3 --- /dev/null +++ b/appengine/flexible/pubsub/requirements.txt @@ -0,0 +1,3 @@ +Flask==0.12.2 +google-cloud-pubsub==0.26.0 +gunicorn==19.7.1 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..398fd9de21e --- /dev/null +++ b/appengine/flexible/pubsub/templates/index.html @@ -0,0 +1,38 @@ +{# +# Copyright 2015 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#} + + + + Pub/Sub Python on Google App Engine Flexible Environment + + +
    +

    Messages received by this instance:

    +
      + {% 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/redis/app.yaml b/appengine/flexible/redis/app.yaml new file mode 100644 index 00000000000..b276da3b662 --- /dev/null +++ b/appengine/flexible/redis/app.yaml @@ -0,0 +1,13 @@ +runtime: python +env: flex +entrypoint: gunicorn -b :$PORT main:app + +runtime_config: + python_version: 3 + +# [START env_variables] +env_variables: + REDIS_HOST: your-redis-host + REDIS_PORT: your-redis-port + REDIS_PASSWORD: your-redis-password +# [END env_variables] diff --git a/appengine/flexible/redis/main.py b/appengine/flexible/redis/main.py new file mode 100644 index 00000000000..24caf09ae29 --- /dev/null +++ b/appengine/flexible/redis/main.py @@ -0,0 +1,54 @@ +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import os + +from flask import Flask +import redis + + +app = Flask(__name__) + + +# [START client] +redis_host = os.environ.get('REDIS_HOST', 'localhost') +redis_port = int(os.environ.get('REDIS_PORT', 6379)) +redis_password = os.environ.get('REDIS_PASSWORD', None) +redis_client = redis.StrictRedis( + host=redis_host, port=redis_port, password=redis_password) +# [END client] + + +# [START example] +@app.route('/') +def index(): + value = redis_client.incr('counter', 1) + return 'Value is {}'.format(value) +# [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/redis/main_test.py b/appengine/flexible/redis/main_test.py new file mode 100644 index 00000000000..f314c04487a --- /dev/null +++ b/appengine/flexible/redis/main_test.py @@ -0,0 +1,35 @@ +# Copyright 2015 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +import main + + +def test_index(): + try: + main.redis_client.set('counter', 0) + except Exception: + pytest.skip('Redis is unavailable.') + + main.app.testing = True + client = main.app.test_client() + + r = client.get('/') + assert r.status_code == 200 + assert '1' in r.data.decode('utf-8') + + r = client.get('/') + assert r.status_code == 200 + assert '2' in r.data.decode('utf-8') diff --git a/appengine/flexible/redis/requirements.txt b/appengine/flexible/redis/requirements.txt new file mode 100644 index 00000000000..9af9871eda9 --- /dev/null +++ b/appengine/flexible/redis/requirements.txt @@ -0,0 +1,3 @@ +Flask==0.12.2 +gunicorn==19.7.1 +redis==2.10.5 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..4f0df036549 --- /dev/null +++ b/appengine/flexible/scipy/README.md @@ -0,0 +1,4 @@ +# SciPy on App Engine Flexible + +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..e5ac514e8b6 --- /dev/null +++ b/appengine/flexible/scipy/app.yaml @@ -0,0 +1,6 @@ +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 zcmdqIWpo`o(?Y(+ET`HAIs*+SKbwABKtpI4Uk}{G22m}J8!9U<>4M|%@Ow3SORY_7t zUILr}07%mER`w1MXaHbm@9L~7B}$_GT89L77=Qra0T_S<0E|pr92L|g)d6slkq{$s z0W&@0|D?;G09XaeV0su%E%*|$M=3)ZI!C-9b;p+HIKL%q= zW1D9T0riZX!3u)$%V%uy2d4kc^9L4w#-?`mreL0DogGc>OrLQV7>ByMnSn8+JQ%-s zw=(kp<4G`nW$R{V1;*E4jA>_P#a zSF^WZJpn+>!SS85m4&4%3AG6g2@4MoH;J^Fhpm~bD}%C;iH(u7DT$baouiTcI{^5j z&CjKP7tei50`6o^79LJc23AIJ_y3*#rM1gN1T&yfe zp2=dW>Ley^&hF0`JSLtm&;SB}3Sa`b01-eAPy-AAE5HTt0m6U;APXn~>cDHj05Ab8 z0Xx7M@BqAl0N_0k0elAHffOJU$ODRj3ZMq~0(=F&13!QPU=)}F=7AMp1K0zOflJ^H zJdweIkUVlepT7^1>dW1%XCV-}c z=7pAl)`qr#_JDp5od8_`T@T$2Jq5i1eF*~tgAGFk!v!M^^BTq)#tSAACLN{{<{Qiy z%rBS=SXfwGSUOk%SS45^SXbEhu*tAxu{;YZ*%;O`L75vULZ5Y!Q@5dsjB5GoM55f%{6 z5D^i{5P1+)5UmjX5t9(B5PK0<5pR%QAkiX;A?YExA$>wBMEZs_i*$yJj7*6vjI4v~ ziu?(=7`YR93HcfY1BDSq2E`P`4ls<;$s;x40y@61XE1&OVQ4fHX+~)wXgO#tX;W!O=pg7g>8$9|>Bi`x>3Qhw>2v6( z8ITx67~C038CDsw8RZ!R8NVW1eSu!6M5N$kNPm%1X;> z!kWrD$%f1($>zt_$aca`$8N@+!9K%*!J)wMfun=tfs>2VnX`g(hwByB8?H32X>Lqz zW$sAsULI&3Q64{@uRJ%roV>2QHM~cB416|xC4AfbRQzWAdHlZw$OMc8vISNINd(^r zW(lqckqQ|JtrdZ}!mP^)0wo494PE9UFZcUz6 z-dVm`0YX7mAy#2QkwVd4@rx3mB%>6ow5Uv_?4;bH0;8g=lB%+)%A)G6+M|Y{W~f%8 zcBw9^9;H67L9OAY(V>Z|si#@2d8H+;6{EGH&7|$4J@6XuwbkoJ9atSrodTT;T?yTI z-3>hsz4v-Q^{MpV>h~Go8Q2=M8loB+8P>jmexvoKQu-d{z4QC&4}2doL!m;gLWjfH z!ji)t!%f5cBbXx+BJLwiBKtqGd`$ZI^vV3wa1>`$#%Gw%_Md-73q=>jpvQQ|{EAhG zZHOa@3y(XGH;nH~U{A6IDNneAEI*^@cR)M7+6>sI0Q5V z1bBD^OcYckG#t#AFL5xjvGE8gi1F~r39zwA=t;;asc2|vaETe18K{{lsA;I5g@C~8 z8Uh>w1_A;GH6At|_5bhm)B&KwfxbYJK!V5s2y_r6I_Rkjzz2sk5RlK$JAWr=7!VXJ zBm_7U4@?K={WB913K{|g^E3w_L4pgSL85_UqFw*L%l@AUki7ZN-#sLw3jcTNLs@+7 z>rwyZFjd{X&G_@~nqu|pm@99ux23i33>U$jX}d`lyEDW8U&O$?e**Z{T8Bo)G`3sf zBEEisZ2FVUdo7V&*KL85Ezkh9SdlkO$K7T?L9f2Q#GLYN8&uk6pU$AF>%tG2RjUu_ z{~{PN{an!livit`*ZxSlWp)yt;SVO3lq+vZQT*hfqc&fQjRF^Y8{0UB4;lU^m;5DK zp0@h8b-JE@-&7j@MI^wpEWYKWI)-&taCjcknl2nO`kM>(1Ao;|HG3V zinX#ww>|FZ%ZN0DJV#AR&6wbBqHUmGHeUHak+pyUU zzVJ~VVG74RI1$6?M)4FZ46&NpS1q-AxQXZdAurHI%3*%?$`YLUYbw(Ala7@C(Q-O{qSzJ9q__Wi<|xKuniL*WS^Hjd9kcu1!r z7}(rUm>q`CUE1cq%i1t}c8@deza2~hVsY4b`CD;a=0)@CzKI0}lQle>U&Boattqu) zJF4i1rn}3^7`H<7!KqUTM6QBC6!t{}S}CV2?B76<;eJDX<_Om7SN+!OG^L5jLyv-O z?raHo7ZqQBq8N@gjSTs`(fpaTjkSmT1f;=R`9m?lmO9_23X(O{kM~KpEZ;O*9(?qR zsTtmp#9<7C=9G9=d>=NEPEv;ln7OMH`slkO8BM!K2W?wzmh{ZO|U3W%DE$8T( zcW+Jjm4aJibUt&H`RI1SlVnLPSk1qSTs0;NX_5H4y0b3A3?2_~Rw)dj#_yMmuVl7w zuE;D2VKcp-fGH?E0TfLl6S<9`B7e{@uMm4>ejd%?;PSS|K)aC6>E+4w9r6S6PGcIe zNdf>!iOF?&SA<$9nfX_J^LZqccG%!GWZ%A-=7g~u1;hu$WC&k5Y&f_Q`HVU{G=T^G zyRVkvHxnM75=+B{d=ZJE`Iia@IIQ+NpkxTXxn%)CH_5)X>ZBK10BVNcXs)e@sE_x- z7JYr5zU%%ZL(-g~F%5V>1Z(N~uE+xab?#3CD!b;pmpUnFBc(2$_&N|3LlyG_%?b;I z0fLTz^%)(7b;7You4SXR+_rZYN?Mmt|Gei7O`p}EYt)<>)Pg_9^(CDg_w|t0@{cE= zO+kF%=AliB-Sgc8jOdKs?5=`oQ{Hsa|!!jSPL!d26|rQEtq zSgJGYyRHUl?x6ni-PhE9rMPZ0x@||U zMU8nbH*HPE^VWpX*bohuN#52g*SFE`nRn|s38`L|y*?MTXML<@81$gCB@yqvQ~EqUlwZ}W7rTYx&BbXB*DHK>1+H9HO+VM@^!|{0O#*uO-(A#d>K z(!-WbjF?>cP8-4BfXPHsojNf+Qa7zspR(EL!{s#&*xQ8 z;>F);{m>S$g~-1tt63;4Ws_;AzvN|*b|I(C8wsxU-xA~+HZ7uLTx7lt=gLm6RhmA@ zR(8;@ba~-Di;%gW5rUsCQHmr;{;lMHXn5)<$*|7#ENM1do6hjW9){Lrb<=Qg>(wEl zv3NLh?(UM3{{P|rR|G;=+J1B|J(FlAmdum0*|fZvpTBp5CO1zkG|{yjZY26^js4fs z|5p+Hz(FfL8UTWPPEdb^wU8k2{x~!mItlv=3}#F$QWjR>H)L$e44QQnQIFUFp-)dz42RGzg$bhaUCD zGQTOtKyV;gDYQShVLUN#uSLsIYfdw5Ox7d; zj1=hX%|jZ@;JKAkH=J>%O|v+Mm3+hKLcOGPuD05EWcZ=wHcvL6c8ezYYC(iKjCP?| zHatS}+NbUbFt?z?&M;ile|649Z{0eY%}%qHj9w_s+#>flIjqUR-`>fBCPqFP+(B6i zK?};v{iAlirZyMrsFKI%-|8JF?vOrAypcadlNYHy)li`s51hLg+o9~yv3hyL+FGo? zsrA-1*!m{zvLQkHMa&zT_xSg0Jlb`qSF}5dG1$|UQ3^@0Y6jD9sf2PE?I3r#)`^6_ z#V|&>q2-ZnV5tW2|0bHiFL(Sr7zr7RuG!*{ufQd;vG!~g0}9+M^zdI8G-uKf<=jg4KX|mS zRaJ#+J#RO0a6BJ~udd6M=hcvBAQvj#(K)mex^*y}kZFc&N+cS^i&>TlXKVY$TwbL+ z^ake@K$n*@ud+;P?=A*;h^4g3LOqEO5J!^=|S8vA;Mseo|iC3LTxi1}~wNFnfY_pYQDEoY*_# zV5JyeZrJ3;HsgJMj9!P>bEZtqi%|Hyd=j>K*xRY1x2^ISY2GPf(f-XJUCLOPSc}hD zl-@nFMXtI-LMcCoybu*p>lTs9cYzb|bAqn5YU<#9r7ee-QRzIg!Ho4~r&?_OOfA)e zK`T9Z^KyhX-#FJIXfZ04lml7bqpJHyMkWl+${%KKH$)Tonbo&MRd2V0CKr1%v3_F}V7Sua_ z48g&u+an=!sY{1u#}nAZu(UFrcW%y`#;-ymfA!HcL^_c5)C*XmixOTbUqo=;5C#^e ztofcCQ+mKn?$}Lngrus~e^^O6EY_|qsHJY74UKpL456m32LuQD{RT)plsy&`rIY?r zxS2AR3j+6}aQ(-+9~@yT>QfXU!nX4TRvS%n+wyaerMqgLzczafp<>(v+vy_kh0iQgyVSoH;CR~BCv^yaqE zr3Um?)Gmxmq=*tb8Z=uL%=|7QgWuEI)QY}StvMZ5YJs%$+6R7%V?ARJj5jF4MU3)p zEC-&p8si_i$&1;f?HaaQ3pS5Ysy8veNWPWtpJn*l|a9RZb?Z z3zj4$s&qH0%}#28lrl$Jn&RkP?;{#(u&w{R|L3^S=PBCkdl5W?zrfX*)Q3DvD@WcS zvq%z;vbL$*JI>{LE#O+5voE1JK}lGbh>_3GdtRHzz#rwi=gpX2l9G2lK+uFs#D%ac zUg=IKcp#NzVk@pi54V)|I*u`Krn9v!CZ<(YZN14#OFvqoWW7w3RahOmS5dg=Fv4J) zmtt%{g08>iuek_Ex^;B}OGV6?d5m!=zlHUq3)|SR7NhOS!sId|KFSBkjw6zpHC{|i z?6a@}g2s_gPXGd0lig?wUmoNS;9{IItVZ3v7dLpNz2{FSd)Fi~-D4kEnRnb;bRKOd zk>ej|2&uD0@8z;6f(Wq;dO6OiUq6^HVSN~dR;cYbOURW3x9dePbReOF=?CiQHQx~M%?i}vnqa({PWVvSvEK`s!W-+|G;?Y?diuckwf_9D|Jzag1!RJu}xEM~#2 zKxa|x3v}K0`ml9&C%iB#>xps;*~=UDdE1ceIen2Y{;pw#VQM&T7?vz&Z-h_ zoNTHolMa!i9Xb@{RzGjxWgsM2XC$&j(5gKR({vxrjgI}Vyu3Xxug_lwB3?F?d*aRD zOdLo^=(@(`eUT~9i7cVhP8GwK%*@%V4BS63U7K7q^DWsTSDr?%iDK?9mH&w2L=b6| zxj;!jc>SQcKh1tEG4^IqjIz5lvhA?ftuSi2fXIKtMmo8-K)7&l_q`Xy`9kp)pBFSun^%nAuc>l~s+f zME!pU9N@4-2=oN_v)0}@ITESuwuiGe-j|hS(*>Goor-*4?Af!3`@xFCy@B*za{Ynl z6%za@&J`yT<8cVvH2t~u=1nb;wijhu)K5)2?7Z3Yj019-BSs$1@XU=~Bv(15j00t{ zD3^BK*L6Cv7URXfim7VdQBDsrwU+giBRPsNDm*+YU(~8Ej0b`2l3;eDzuyMOt{ESw8^iFzIs1#-;(kNPk!ThHX`% zGtbY%T`c6wdZA$Yxo$v_5)M7Mux2XCB=+j{tg=2oCND-J zR~I{Mf_`L`kYX^`qX&h^k6d!5_z3>YS@X`4KF-b*oYpDe|EQUMn{Jr}*^+>fYxA|3 zeAG^^9^AV8nMPY4!R&f7C#x|$M#e*RXPyrX>ZiPtX1|)fAm{UnO;{T13M1`qI$6HK$5!@B;HS8P_IlfZXXMNJBO%*w_%>P`d0b7U$)eu%M- z)y6^ZXnbDv%ZB4fgPqWVhZNG?`vs{ZN;?F*4+W`aC7h|M-sduPbBWeva}09TYCZ*p z%=}g~Wi~K)80$7Ldg@Xw;L3~`_<^t!IwMSjkw(KAIF*V+!HqVO^t~9VRt;h&uI%f zzpIp~Ru)p{zSk*fmgKHNq7oS4aZ4Hh<)6{Ryt;_dndxE+PdH&6dfHS_VHP%J7`%n^ zt`6*J1guSCUn zJ%UNzsfglQC$k-37e&D;*J@@^tr5PPS;6k69~(zxR{57DO(w(&c`f z*rs&outOTYR-d#BYVS|LmSx^xgCx?K7<&bJm{@c1K5yXS>-YpLZRMmCj}q@l@KNAe zZ5NfB3Vd+Riym)LtHhGzo{!GxmIFCBjqMQfmjB@^v(~Z?f_)fz1gwnG z4$dW|XZulQT|Ms*6O1>Kg6W9~-y~eY*H3V5_{GEvR=noV$XZ*5Q*U))QuIpSDjwq) zNHDggw=Uu~D{jac^{$l334DHsqW z^q)KWf6Y@Y&=@47tRk3X%qrw;!pcVeqN=~=DTqJjsfanS33IbA&*$FGS7{@Hc;OZs zRh7ZBU)uOY(EW6a>}FhN^yO-BzZdnItqF2%o(+FEM|3cqiF|zJ31n zTz&5Rew{V90-hx1$Y_V}ULNuyUR1{oG(x}uEUevx5XO7P&Sno>kAi}e#5?N`;oVdg z?!rL}t{nM2Al8tcK5v*uwQcDWYVoxLqUnxkIgA``=R_N+`b9p;fptt;AyQ7^gGzZe z>UyVJmT~F~t?$v^xH!)j%(%*r%^&j<2}6!;r?K~QW3_A3@K`k*&|G`uIIFDq$U3;N z^D2CJp1`kN!Q&SG2jb6ZEa|HkzhHZ<5KwhyE-Wbjml}Z@Q}2g_XX_C zRygFeHH_-r*`f!iD9Ru0vCDlEOwPunA6RwT2Wi0eqfyjLVs#8dRxBcl&2#xuYvdA( zn`JyXZevzLw$XmSmmj58sXAB zXy5Tf^%oHxK%TOU@zpq4{=jPt_HODospICRrodq4*FQqC&nZkyCH2gYN~X4s+$PC# zF3H_w`yxT$nOS;b{RI5(rh=DlxgmLG0L)J+I5hX|{Njo8w(jgsXu*)eZaiGHoM={m zzwH8+vX^!xC2l^*wW&o$=t05L*lEGD-xpnUZ zNqo29?`2```pYWar0hZ(H5j9Z+=@0Q+3}U6DtGO-Cu9Yvw=<3y`y!EE{?&SOYwS;= zU6fXm<{_{0cPbfXnPXTzNlRRPTB&z&DHoZNqYu?D%oT^`bEG>bl62d}$QTtYEx)xo zAWdI%rGcl^j@A6dz}!^}-VI^nZwYB3(As_$4x`riWz2t?OGn#ffedcxy8jcg{hsY9 zmvM+~#Aa{08hPu*FY1o*CS+00PsOCBZAN?{!fJ2jK@v&?29U+Ey+h$QHB^(m5B+(# zaXO8UIUVD?h+<##-dBhzTiP#=iAYZ)D=HP*BffW9#?4#17uBfq;O6 zhJgO(yo8SSf`pWeMa7AkRm3QOEgOSe_)WB^vZ`@)Ojh8Z+cq#l&)YU5%&()q9Jvk$ zcsC5Vd5wg4g%zx6j3N9O)pS$xTQ=Pat5jxErY-XxncO{q7*-f{JY=&E@x!sYTTs|J zIX|N7iyh5PAg&?R2|8>`&Cds z3cU+!6%AevQv1Z!d2KaeoyH_un$uwUozZ! z&$IrZ@iuL_wQDJpSb8fXv}rcQ6mxNxxQ5I@DJl=`{btV(k0(IL^pAWSIUU}tvCp@q zQzv+>!mYwpjYJ$9E{tYLzl=$kz^$QYf(^p(^mb$lo!W(Vl`MNj1SPii-S6Z!xyw0@ zRNY#A>C{$Fi^C{e)fd)d)u`3Ne62df1Llm&5}N?B=4-48Z>MN}{{ezUCu_>qrQT!T zN0$mJSz`yK2#$;uh&Y%VI{dzB&c+bx#-8RhHmh7M?To@*TG7@gV6E(o?R&|+yvQIM z%w+$;_a={q*RYRqLrswrkhKmvMo04CPBnhC572zKqG=SZ$|XKN$izu{@Lh;))-z*Z z^3tHN=0YH`c)wN2Y}boP?GK_7Iw32~wc{{brj6dGd%Ew~ZbpM7n@h-H@oW|p@yQ`e zeIclQwT^oEhSo&q;4Z>UY{F^h;}Y{&H8#SAOfePB+}))YJu@a+z;WpGo#c7&B-=8# z`mB+=_@If6zyfF@p{Tw+Gg)aoFsZ7ZHRE+wvs>$$?B;SIDATY^R{QvF%*Ybc*pjvm z^+uIv=0htBS7A3R!N=xq%Ove8W);s4+LjuIw{4$~CZ^=o;Es&az}2`fOoE)!mVVeN>^r7j|MQ;y5dRIm>m|_7h6h7oml%o)PzWkP13=N zH=7z62uf^pt5)9Zx5%#Aw0!$&C^LQt^|UGbQHx!ufAhnQ-slrR$LXtSd!NIee@Eu3 z=(MMBU*7Jo$CK*wf$cg4sDcH;B~U$VU}$z6S8z@DcK%QtZFV!E%8i=cl;$Q1B#Y=B z&;5yVaBf0 zumg5+YCcsy1V)Z1vk$<2fkSP)%A&$`|K}Ij1ibh=O|WizJ&9WSMy*GR)u=%@p1SoY zOPK`dMkPd-_+LOvG=J!VPQz#k^||`%fw{aDty8*Il{T(#!(QaP@uQSQPfPq41f8+T zELib7eZC@1nR*hi(wnoCP7kM|*b{L) z<3zCkvXIm3yPPJM+D8^8Tih<;HuWuR=DJV8Jg8E~`&;Q(dY{QH(T=(qmCT?Kj%UJ3nL4F$ZY~_LmB5J@O<7 zU8+^7sIChpSd42213xzfcKu-tl6AEtPgR1!j=LdmV~uwDAJx(m`Q++G_G76%_>$gpl-QQ}>xw7{s$MYkyq zX%=s?{K2z%!wHchrcL&4s`G=FsT<-(`)Lhq|N52o!YJm3_EpZy0z!&myRGK+E4_@} zT72@>Vs3eQHY07KaUsMQUoEQUu1){ymqNq zdd~180#;u4KIN-Lwb)onsK#zt&@Lv zJprq^zYjvdM;m`^O#Bt`qXVR9BrGCE%qmU+(b?7C$4-Rb94lw7{&f%{L<(L}JCIdB zPYPHn2<&3G++O5PBItjJpUj1GuW55nbCp;v@^kSV--^&k>}rTecmf(nvx|{^iz~-C zu=U&5{Uh`O1qA5M`$OS3o`9utRHk1w!pL)0b@qoRvSnqhDw1;SIj>TE)&0rIp8)4` z{`Sb7Z_Ra6%Kd2+GexdmamYs!_)&X`J?*;s>&2Q3R7mT+{_r{^@%DZTOkxAqCD1#s zTNY{{s`QJXd?pNBht9rr4k6_6)t?s#2bl*bGftB9Pdx5;5*c=X#TI~($ZLj%_|QII zh^jMjTF&C0N_?#}ewm8p^ZYd>^#|d*RT$na1=zdPQ`cR>uF=OtRm!(Jb3tVs; zXrmd=yF)!6r#LUlf-uulyixRN-Y#u!+Kl*B$9Qr%tjkI?9!SdAErsTJG3G1IQV%S34_yn-` zL7r|~t0ZOV$M;|Q(k1TSYthYKS9sK(=p9+CH7$*FyR>G8Z+Qt}4TH1O_wQ9agXv!m z&*QkO4=p-FR2h62|1h+gux&U;BOO8>-L2RPNj*m^r4b)SO3i3BO}F^AEm=9(TWfCz z6YAYA*7iG-njatBli&?;A+jjeqT(htdwXx<$;Im;Oxti~V^?nR;?L4K9NIu8{)gk} zG<4wr?KUQL)&BI)0O<3gY!XFT7O5cFg%*+ASQCY<1r}>(?jj^iW(hCaIz={`eALC|F4O@*AoZBp4aSh$Byn&NIEuyjXJ4j$vsNkY%+t!=2)6GOkPmXEIGD7G zA>#XJP`bkq<7AdQVe75yyX>958Jo3`9dhaBOLrC3?R#=vxg>;j*R$6C_Fn5?IsBpJ zL@`n}!o=x7=E!v1I)wXMfqgSNq}i`mi3|MP3Uf(e#viy}pnUYEiyWDNlT+hObbZ?=a8SFJ=keaGwDfiBU z5eMZWIh7ADh-{25nqL7tcE>&xGM42>%nOk2fHVn_{_`x z7*i_|I%sJELJO(W67k~2UWD)Nxzm~2#XPRr5Xh0~3mRM&J{{IGUtX$zWAh&Ec~?BukHgHsBPVk^gXF~^w0`JucO_#OeV6Kd3Y(n zp7gfivajbQLWI~x(TB;o&|O5>cdA9gHKvV(7;Z4QT)ec{87xx)UgLbEX(9!CTU4=(7(q6s2WRr@!v{1pdiLeaFu~|@B?Kmjb9iJjIy|vO z2tT1+WzYGms@z<5qH&!DEr;7+m)POA=C8LEv6^vJ8;1{QQKPNZdH8vR!LwO1ih$`x z$AI;<7pv%pd9y&cNFP?}7c<3ZeB=(8B?BIqfenwC05TFZ;STy_cONNr$H;WTqhY9cc&`Qt+e>q*H{Bm}+$GUGeG?zsp-z&(0B#50KXX%ks)>9hFc#5~VyyEzcw`a8zXY3RjowmTt__|Cz-Uu`uRRQ#ZrQZ+Xo2*IqoW*@ne zrcUc!^2t}X6`iDogW=+}S};COudJtaBGg=4a5-%>4$oAu&L@EsfMi18XA56kKf&f~ zH^%ajN{kRnnB<29^=xlsc`1h4Ubb!_L=!8ONf4lDy}G5V^O&rn>8&pH1jG{;l)pV4 zYllktXm&cOO@vZ}KIovpgg{wO^(pGk&~xeu7@W)8m$atHbiW0EFC+~GPy36i8()8O zGNI>Z)NI}^R)t{*GL-9)U<|u%>i2NfB?0Vh{uGxnEh{7EUG#jfu&&$0orq{v-O~7@ z3az%hR4KLO-U;)3V)T5^m1(sEBACw$84AFOnU1U8C0$?_@t8ja{3t77neiptgAP5c zdm-o&AzA>r820DULAr{6ioRnZex;XCsT@h!Om`S5BRiIVzcB50$-FOU`hkVzF*lE8 z+3PLrP@-a?x#Y+Yu#^|DRO{KX9)W=^XSQ{;Ge?>zjQMPG7%=_9LizRC;mNvsFZ(EL_2!O_p} zx0!+G101xo5&r+9|I4k+|AqeSf6>`+mj!v2y#KoWXM1ts9sa!CysRI20v^Qt{+azJ zKFyr^v#@Rh=Q4d;%)-CPqIod9l=!0ci2Dhc;m=vS zKO5G%J8G*WZc&-xuig??#ekqkimI>N^Ed+CV|r&fyPiD(tu9LZ$0iCi55A`({Mq17 z?TEck*F=eRzn=ZbI415}t{(SDe{rTr{H$14o0EepxaL!|7vNjaYoXc^{`RAQ$9RD& zXdxg!+*g5GDLYGb$jXW?yNQGUII&VpahMg*>;kPGdLP8Ror z8uTBA>Cmc*Ao3O{>E9*%&34-1cm@#sefwX5)1LpH=f5iZzZD()U`2xtJ~{zGfIpM? z>kTsyfKH0WLL#DK3As?O*D!jda^6EvLhn2nef}->y|Jq~L z#H7!79y!;TUsSp=fxa+i%}GKh&ffbCU(`~ncwS+IZPV)LeOehFt8Hrg>dS^1ER|qu zF@^+Zyoumq^*-*m|Ifx0ckmLiD{w^72&N}Dfn!Cn^pO@&P2M4}3 zGA*UpzsqD(@&><_y{V!XWY31qKdr6m=;b=}jlwS1IV&gvtIU*ZR@18^|2waYR7VK8K5cB)PDGU{tDW zxQf%5yLygC`zHC5=M$j%BY3c z^d54(XBI=qKFNRKv^J{~W4dgw;9tj{TU|=E1?;*u z;yU-3SX@j_o`71cs0Q-%YnT2kE_;T047X+M*HkHH5Zc?;538<>?n0q<2vY;bAX!0a zGfY2~vXPlgXm#%S6=9MY3cc(*-E#4guzCMeN&%-_$eBBPn)l@O!}FYtx=kR%lL7Ep zr2Ts=g1=(={r&}bEV7`H{vM0j0l&v$wg30yliyn$&qtcz!3cnyXapA(?LHme<4I$~ zHOPE4^J;&umg1KZ*f$s9a=v%%VrF?WL4bi&*`=s{okThg-WLc}OduZoe!)V}`@$M= zJ=}!%F^5ibH~a@(+8&!-Q>$NU(5{c@V+zI658u9^on5Llbb>--<9Ub4p+X5hrx|s! zX@~$@(E;}e_pJ1ypTCYm?GhZ7Q;mM^dMMkp4DyESTST2fnVO>@i@DS3=Ju0ya{kQC z4Juy6+Xuh6NA>pVHvQM~u1Az#@shw}ZZMtV6A)frldnVLh>bf2IVy>4xGayiDKAJ$ zN9Q9{gj?oH$P3x7Pw)e74cmN^{elwN4Z1qrDBE8Gm!x35ba3ovsQuA5d0%dc9}ipD z^CqLlCga&!)uB6sE&(;aFw$Ed>&lN6tMJ4H1I10MV=uT~UbYx*i9z>MGej|fALkdt zbmI!-Q#@l~n*Cit%=l={=rd(es_^i0*vN_0KNPM-seKf;iWe@REq&z1&-il+PI5kO z9xH#4ooT&nN55@>THyf6^Q%6vNUnd5_?ca&vjEK&+mI3bz0!qdMPbf=W}%nLs+ZOr zPoqL12VUNtr}{_8=;FBlO_0S$0Td+9j*8tMx_cdn6~Wz{^Ygc(b@#{kg~f&0%#K!s zWT_S8qHG>&fTOwdAUgecEA-83-bDH*M@awhg|GRmSxlxYvkUpMr$VAfH({TVJY7o7 zvLdHBY4pu#`T>kI4OXtC;YthQc_q;|)61UQB3#hfS%^jrjjRVv%u>$k_~e|skZ|3b zp~#0PG!id%-J`fFg2VY}2BCNxy4DJ76&}9lsQFcWTsoa-G7VmLHM-QFV*-EW%F9P| z$nQ2<&c*Ue)OtG4IbTt$(fe{@G>PL1<5f|irMtBYT`DMav51~7$jbrRez_1Hhf%^2 z>Igsk+d&Hfz{uyoRvGp-s1$SlCCsPHMuHFEg{-hW>jytSwvpSS|U|vWZbXTVK~a7J3ROKk`^75 zyM!EYlO$=Yc7|KCe7yDNz9R$x1qJi`UE%LB`Mf-`h^U~E7&$S6_Z`0KF^1{&?4{$lAj>aLP^ zD9zI!ajnzR;^c0;7I{y9a-aH6u~Bww_NT?eK&-%}E~5|U=p#cX<UvvYX?R4_E7z@*OAxT5MfOSqFs>ywLb9q8`r@2%({s@9 z-muFt3rQnfURDy*+Zi8=8K@7cazfG4Zx_h=Oo-i5Q?*g_y~k9UJiL*we!rVCF67m# zNlQpriz&mL8jt=+DT)*k$$Du9=NyE7fQa-`RGWk>MBU zJzG};9tPsBhK20vSRXOs44f3~x8r@Tryt}7nd#J~Uf7K-w7g9Z;uDK)@s%3$OoE_>%w9zH@x+FgYZ>BwzN~r1w?c;zl z{y__Uu{YDVCZn{g7<04r?L%Sdn)PNf-Wq@9W}PfostL}Is570w3#wOA zUTT1RiMCH(ukogbp><01$9v4j*IkU15Qa9F{!rr1itaRM=G8rA$wE#rtFCZ`m^HJG zVkVKg5O#0HYY#6-d&;vjd3&@O2bDON9Q`ecW6vYi@$1YS&1db|zo`+hA#86-feJB4 zTC__ddN@nU?navLm1^!~uc@)(TOFT`xYO`~prD9MV+S5vU%SQfEYW(K9hFhsOp{(6 z#KR~yV%XRpYi>_CP7n_X4PXAYv9pF3Q_a0d z2R0QsBmX`Jy*Xvrz<17z;#|fOHp0cMQ#~DqenvKIrb_LZbZecjdxQxv(aETeW}a7_ zQ4`zWKK)^xm7a1#`0m|(3D^i&syD~1io87MW|Q^$vCf86GHdH@xeHp~q~9B%RFO=y zEfL!;ref1;SDW|Kh_b_0IVsEM7N23ptIvH64p9^qDTj5vdW)il4cMK#g+KwOY1;cM zruGLILf`P6(e53L;e$@&(jp)+U-d(hwre{Y|6t#gAIaTHIByLZMC?3DM^dhEC>&ea z5IBpPw?k^=O$cu%gkVb8$QtE!hF3L3d@XIhmY|L89^YqBfc%o6Vvz6Bs0aL60ZSzl za2a`re9zkuwH0{G-dY;>nu%OvEom;my36xAGj3c3^=HH$@Dge#H!RV-nqaG#*`Ake z?0Q{e+8X?EAORM3x4fH3OFKyyS7z7C?~A0aVsAoaMLq@cg})h~N-icK;@k@gYpSy; z+kgA<)4PNA7*-8@Ev;}m9jGbI4V1&Gu>xpv>N&c)m2BUdOVG#UOXF4@1Sxx^|FkAI zfapOpt)5tqhtSO=`yN9JTkfzCrpx*B{}bE^Blc1yjLmq$p?Z;WWE}?Q8xXgw-Q@s) zz?zd@3Y5Tyt7Gu;1vb`xOW=D(eTM!Y(CfnlBT2i@j&M0Uq`qMe4*_s|9af0w%R}OU z6a>cP$+*$8slv{xW1c)(>ly9(LSzDf#vMvWD#4*ElK zS+^Uusp`YY%DjlBD%$6Z#0Mx7tD*Dd0=dl1J1ZAa$c-Bhp}6p)+%8V98X=m|4Y`G{ zUT`czk~&!p_Ho2a7TuY6M9)CQtAY86O6Kt^n=v(J`51Z4d4QC|p$fAi^F4U|K8(^i zPkiI{+e0a7Rpwd{%Z6Nn`*nrrFdaCfMOI$dV8%N|%Qf;JEVK(ja61`t+wuc|IXB$n zzXI;l-U>sR#AqVE*Q6oNb1;>vTwMtH*)KPPDP0g0%&AYcN9TtjCSzYC2y=tB!8av0 ziRevn0MSZrg1;{dK8LY~8sb@r<^KR-mw+G`g>gYM5uHp7rP`~`jzd{aIleA-Q08aY ze#4K`=*=UP_s&0Uw9+;#95&(rgS5Iw0t{7ZTdCogUsou|kA-}Z#w3)rv)IP!t?e#A1wBlHdEqLbM`i#@M0K4B_IiQ#Mkz&G!8AAj-9!*pi>P zRFwG0@_>NQSH+Y|2`8*O|Q;Rfq9kb5Iw;abGWoi%6sP@rrK!|-he$0G#+N{V$ZBmV@uQIXXnZpzYOVOPyYa`k?#Q2)sFeO zc}yJ7V`(*fjc&>}X#&U=CL(yKF=1y>43@sHVB%8Qgg@wo&>{d`y#(X{9Lwttb-{>> zb_Zdj*yHymqvmNCPtoE;I^p^FhdY$3e4aU{N(^u^QS&&N!OPC}m(0LbEbm+RZoI7Yh3IHa&9tSc&q%qd}c4AIyjKt%X zP=$+j8#|C>y@_KllKC7egH{@DD0udFV_;RO#dxRzhe%8ni0;+kEF75C$lJQCcY9)j+jSjBtiDT%Yb+qBWbE?fRtiSb9CG#!3{w|w&maom=G2!jjmwXmiaW7LwQYFa zmJRvo!#2+R$QjdLlf@oBkl)Mq2GDF96PD&QpnCL=h9M1&4nI$$G>%i>IQ=%z$|Y!> z_h9^TE*PK!n)fpF=4F!%9jPD%SiT+2VBEQUjLL7l19PlIX(=>$Efq7wUQmQKKmAB` zR~sjjNSv=VP(vt=vFntmY-;D-h<3vnT6>Rn8pE|s>vC3z+@pHp%#8;b;nGZJ8M#0<8GfQXog-d$%pjt@=2U{lb>X1$Q;g>EUB?J;ms+0B?qPi%^JpD;=ziwSuyE`d9KTPa!ODB*AEw$FN|k~| zLtSD80TTpt;-E^zq-7w2hXs2x^CQmr2|el^-_bz)#SEFhl;CB)FVSPafpLN@f}Er(}T@;g^R%*2urDe;>AbZJQ?qDrBAln9B6Zk z67guo>({JDji{xr&p2frm>t<(V;U{G?*IT~K=57(lC&2PP;F1cC=HP69;y0^7R?Y} zP?`lge)x^O&*l^FpW@}av=8rUEkxs#dK00nBZ1(uiFwA9q34|KLig7OUeOs?FQ`E^ zkEsJy)Iq?$vrRbt_-+$^Z~#1s(&z@DoIcppDpiMlc@l+A)V{BHU}$G2)-P zzliX=wn;-zLxJERl$b;kx75~JVtn-u<^CoL&CLANr6u3suy3EfmIu68L)HhOlmGw& zAc49D^RL$$M#=8rifO;=2LkX0Pku|xzcj$g9I@nG93Qu`N z8-JRTj4uBG1(&0P>z2QXU&+^v%wPs%eC$T9x7}Cgjtgc^ zxqQt$I+enuO5tDN)Q9G@O}I*6z5f7G#Mh+rb9~@nYFz8D%h#O8B$j4+`wckJs1GX_ z^Zr$X0>u?W1o`vgw3Y)ghNLQ-4x_J5QDge6D#BC_ zD?%)n0pd-;h?$HAqETw~>jt(ov^?jhf-Fb2sY!Gt=nr>ZMm%k0Fwj0>m-%xR7CR=L z<6>$;k>t;t8&i&%-Ix*_R%=y<`HF)hDTjaVFVlp24}hrc#Bi>g!noah-PnJcUMDfK zBAUnLbNt7t{nhzr8Lu`$T0K-O4)vk$85-`ZnlXJoIDv7a^9ob#E_B2Ey00+%$^G}Hhx zG5{bWT>xb;^voEl9Nyrm6VeB^H0d==;$smTwuL;kyYXb#|?@9??Lo{WXBU2N^j9B^5OdEgd}rX+rfSfSinif}E0qii(nw zG~1u_9H3;Px_m?Y9`zLiYnq$x%o2fdS+qR&%Ui*Q!`r-)HXcEA^jBF}*{7?6Iktt)2Zd2S+Do&sSdFui*%v;I|>~Lc_u%;y-*$ zNK8sjNzKm5&C5p>6c$xfR#n&3*3~z(wRd!Ob@%l4jf~>P#_<#1CTHj77Z#V6S60_{ zcK7xV4v&t1pZtl7450W2EYjycfc9XKlF|CEzZQ4hA*V@D^hfFBW5QA7Y;XvFVW zV{i6?c{=I7+p&(+{ktsc{O7?IiB~F z-Ei4J6wGq+x_n1`cxUy**%Cr(+95f5Vz3m^+(2d zBcU;{oIJ;~JftWJDKg!?wSm1|a*V|MaME{Z+)Nt$HEw%%mREO+cC@5gDsl7lIWTk4d8ZsaQ0 zLLTTB(O4V80!{o5I8HOF<#tsc2xLbLl>Mj8iMH)-*%g7Fdugt&C-3|^i_{|m7KYiV zwgGXg)}*Y(aw1S_Re7U<{Y`Hj<1H7^M1<6$ChNIPZvQ|1_m6m z!^>yWA*bf15qxYR$_VA)+Ih>SBJlmdH*(~4L;!eG0Jx;M*yVX{$aU2IZ{BCLF(V38a12>2;@lp zyOCTc2$Q4G zl^d=^0I7QW@Jk92xX0jjdLno#e~9#CQS?n(DdL_|a5$$FY5BEMSc)kXorv^J&V%51 zf+IM1s~wrM5A9QX@Ls1n^izYMw&GgGIcvvx>;6{y7sKYUC`F@R`J@0wUekM z&+>lm1Ydy-ND`CPw^CV$;>4%QwK9v_r9_}-qIqY$^7Rd|zM7pwV-CWrsoNauCKbWF zZF7pd6`=JeGvK9DpCs&15n-s^W7h-X!!`C99x-ZJdziXpmk1OMr9v6b6eB;ro&PWr z{5W;%vyEP*+Qc(9n>+N62aLt4>`nYZ$_cWDngXtb#>MiO32L6=jD$q5$dPY*u_Fb; zR+)32ein#5&+3I(XI3+Plj^}!PQOIDSJa@Lm>*_8)GlOwT;RHgrmo#*-^;9c=>Iiy z(Q-*EUDpeIK?Qq=%#jy|tAd9pva;VBdk!RSD~^a6s|t4vgTuX=O1~v)1PmOSj(l zd_LrTI$D}I@(CAT>)|xd&sy~(Yem5=<|fWdKzO|OYv0ss78>q6o%x~PS2GC>c`pep zL?AA)ozQ^P+)_jkft&qEyfKDds=e)Pz4gRuwCSUEou1tlQdY*`V2vt-JW^Jk75_U~ zME;d5dfUAu`kL`Qrgo2W9K7j^Bs5m25Z|#o^Ct&I0k3XsC#na5#%Rk`(8ooUR$b=tpz+#imJTp8ZK2#%e_T6>jpv44sc z^NR~hhuk%e?dX>V)Quk5f86iZsxbC5!_&k-%Pq>@y8wvIgxd%Ic_JE5t* z1bI{Inq&Yydcr;4oFj$W1 zLEMuFJV6$q6@7@nvy|ZwWOKOZoLau&ItDLyqyg z=?NDkD1FtWQaKXbAN5ClcTvkAQv1+}+vchg(2Ksg=3UCQN6xReSTQKS6&cX+8V z%-b|xKblHtrzsDOz2MGTUtdjQ7b8k&GiUQ|5eiuM8H49J+G{ZXEbKwI8&$KI=qC4* zC7j^dzMeytEB$D2;&da&mc>0=ZR3Yut7`8wA|8C@4bA~v5_h(;U8|}p)!L_JpMBUvXp&!nr^$(7Mks%svV zprP=H^AyDXE{a8w{{IN$QBoMAx!Z`quEYcqueWN^9SdDBb30i}=uJO!89e}lj?6-n z)CtuA+1UPpOW(p--xPAq>3s&z{>=nTcXWoUwi8B!gNA9l=L0P-()6=QYA2=@%2JKx zB1BO>Cow2@?}ZPLM6UMQRhS=i_B7gOuP=5XL_Yq$9QV0T55{2|o!DR!>~?ip>-rwA z+?tMu_3EkVclDLuiMQ9_Ue|oFOdoEymx`2ns|)wb$a#1UjWqpwG|E3BO((oX^CdJ$ zL2N_S_ftHtK~h{>QB?d46#^PvT`Xd25V;ZjI-Wko7dlegX^3K-aU7E6@0@csaZg|5 z?y22gHq-R)Z2 zOr@OOFe59c0D&dN*PxWy&7YneML!0RW_wJngTCBhn*sRQ(}_2|yZxYoq?C{5&YaQ7qrN3YkdNpqs<)Y`Gy(zX>U-#nCWY-oFMGy; z13|4+`g^8HX4FZ(9(WcR_g;vXZH9|(#Gt0ZxM*h`sy>)!RFgNCf|p4aETwScl1% zKJmwxH6?SrODNyxfLUyxaF|&Q#*{E~e}>LmsYEo1$@qTb(&hBDK{r+o9ms2+luqX^ zzxskSvLFKO=1*%_lH?W_XhCIcgz9A@BGBE@@+))MGH-i>Y_`JR!EMzF6f$0Yt~Vn2 zx;N0k^5J?xcEkbby!;;W;+wR~w4sG-#e@87f85>3oU2=3qVz0!8hE zKUfn1?n5GQ@D-`>`}z7&N@^rw?JSm`VQY5E(&2d#_%0{VsSpAAb-AK^o>tYJW%xI& z^7_V7W5m3dd$ge+DaUI5M)L>3;aZjFjAvNF4u(go-E~(t<0t;p`bbLk`m)lJ^=}SX z*0I~U9TC`y*t2-JY}Ck4@P+P4?@xn&Szp;`F+s?0C5SAaaE7)Qztq|6gI=O|Rb1HA}x7dUoNq*W=wHr^=JG5T9KHqZht5-RWBE7`s`iGrk?g^&M zlNT*LxS!^alg;aPH23Y?1O8QtRsJ2V?Z;LGHFMC(>N%utXZb8r=TF++;Mo%G6M&-n zXACQ6V_D`X^AyEjWd2CFcSa_??2ateD?lI32|_2lDQeS*Kn4B_%Gu}++}?_X`4ofu zDK(+mRp>&uvRjn-1&cBqd%o|hB8>}V1||5`mVxKIVLG<{7e@r1g}1-)xh!O%W!yN1 zPMzB_-0)%SQvyl^@W$Djjmk%yN&PHl7aLV?<8V8mh^>R(3(ndyZ_dgo@Y5%FsBVmG zqH2t^=*CO1ng-_HHa?L2dDGa(t84GQxql%;9@rW-pF-p;y;r*k&(CunS4bbMynyhyq?}$nrKh=<$?cTSevBCo(0GR@0&wI(OMC#c)JVBRsJ+RW2<_+=5#R~E9kQ?lf3${peZ-Ve3;~T} zd1jkt_szpR2Rt3Nufv$;(%dTGi6@Fr*%)H8mQTjJ+Xo+*yGTsRw@st^3EQMh@vK=i zcmd9 z)Nm&Lx(v+nmqeFHOG=8$;`Hs!^2VKuQF1*JK>@)Z&Wigwlzm8yWMVPykd@I1QHeXd z`_`1ySyW7%L+HoO8VjBrzC@a>9`rN~iuZ72U3`+MlKMO9T;cJ7Q7^QSz`9eWml_#c z_#->PnNLkS35jB-eT})`XunEGW9Cdq>){C*IvvoDK~!e0dVJq41*9>&Ph57ja98 z`B|N}&DP@se(Bva15*#9yz!%RkhsBDyxb%uf&-6#>2U~mY(0RTy@1#M!1CW3PUmm0uXI2#B1|a) zY)L%Wq8jl!@PNz-jSzjA#863~!odn)FYIY9sQ4f7P$@e@dd%jVZ)mb;)I=&aeLD*z z0@pS)IuV#yOh{KLxQL^KZV%anJeer$|GXR)o0!w3l^D4v;dh(lUy;!BSA>LV_#Qzu z|4-@U2{eQVEZCQkzGr^LyZ_1I(?}fN^{;|1SAcB5DK0eY2=?RfoJc za&W16mc6rNc794Ow{jn*B^@QN*Le+k%&rPmR64ArB+5M2)HHy(>D|lV+)mFAXMGSTd+ukinE=5xrB8_rn>L7+yOlbv zdXaU(U-2g>C*;KGNR16&d7H)029XkZw%%Z~HmN<1V(;nWR^x6 zAaLnUF7?C5EE=+AAFLj>G}NBfZ!MoeMus$Va#zcp%=}Es883PK_^vc)iAnC9?J5h?|HwY+-VOl&p-J z?JO}hNUpT!!9NXaP!2H{nLM~irp`z&de#XJw#u7;J9FC9fYE8Tw9V} z=K*(5>ifkWVWSae8o^!yMLXZ`oIsEHhn@=U4z?cL7?HkR{?JhGKpSx=JrY@W!S`%o zDyFeGG>5NAo7&f$PV1Z8-mTg=ycNy0_$A%0=JFzjoQ3;HZxBCj{2|+Q#{GoC>~HL8 z#~JMt z8+vBsQaE+osrpU?N& zIdc)Av9N?&bvz5vx`?BF@S^OMUN3jv-I`Tin*skdA&b(=0LJNg<$Y_hm+c~)Q)>#x z;U+#C(QXVR>7}1Ujx3;A==tua>Vtn}uOtzIUTD0HZBUQQt%&WiQ}IO|^s}Z|%KE^~7kRbs zt>-;*{UQ0=4&kF}>7_3wA~keXknY%k1eZC+i1EpwH3R-F)SR}r8v*P7=#Sq8#$V6x za}9~Lp#*;0$TblnAx%9(1(=7q&{yc!B zw_Q0rD;^Vr6jSGBc_Y{s3j3KHMGSFf0@)2itZMpy1WACLXU|lAW=elw*9OEFKd*pB z8dFt9YO*X}-5R8}H~A<8(ct2CcTvjCzY}K3&zWqKg>g^(&enA+oR=%=Gmb9LX{TnP zqSk1Q2fVH>H~RfuGm4_WdPM!BQz~OtQBO84tC~+wJ3VBiiW_9^_}*G%)Ss5tmaY+=~&N+YMSSXmx=gQQ)~DZJ&8?t-_p5`js3OvzoAyMC)*HHZKn zJ@Ou>%apnpSHsO@1d=vPt5l?(oAlMthA}C6Fc!5_$J6rFe^jN^;|1LMYd{ZRQ%1RitWZpENOd1gU0oJvXhIA;GGf@wG2 zq5cRx#Z0DU@o}|_X)%1(MjR{-=G2VUFF*x~-gKO{fBY0z^+;1lD`DeldbK(yGEe8( zLQzs=C1~AP%%%A{S9Z2c;%O~Hd>QYEN$l7j;8%8*T(pQ)%Xm6cmGDKgUngUyYfLD8 zVjo>IK7Qx1i9k0+OQ~iN@CPgN+CjxeQ6iOv3qNiN6Q1CJ4igmDQuPUy?>jCgb+41* zhmHDFlQHT?-N*Q}Avfuge8D-#1 z6{hem=&MZ>}FoOBbjW;XU!Kwrke6w$?ILme;1fHi`R}erdFI+-!+> z#$h`(j(xs*%{^&AM&^_VU>Bpts#R6#ivZ3L&vwcWO)0D6LyH1tpPrv?!A7fAq#6Y_ zU2(#W(98Zh=GhUOnv*=si~Rhr>kL$xJN~Kq|ED{+|2U_ULLF*fOA?A2E80V7TCiT= zwzK0Nxzn-&$9K#=uJ4LW4=ioE*T?QKW{zVy zU?JT+okG3rU@v*YJJ;p7AAnYIyZXXN%|SwYo??IW@tW; zXBS-)r%!4RGTT;5_%q4qyn}&^7^RDd&hZpqd%>8w$p_YJP2640D7xqnxbwzIP8%jP zLEbgXFt55Mes}2(9QG<+Okc=FNA%>-fD>DS);m=MbzxcBxvpB=2pILO-kPwD2~(m{ zG?7WC#kz@x30N;1nmf9#dJE>Sel_ENh$Hvc^MnafiTD+N_|Q0gWT;_E!urtqKOF)7 j-=6CTlpei-8t7)fxyA(BdZQea9wG3V{C}u{h~NGfs^FRv literal 0 HcmV?d00001 diff --git a/appengine/flexible/scipy/main.py b/appengine/flexible/scipy/main.py new file mode 100644 index 00000000000..612042c19de --- /dev/null +++ b/appengine/flexible/scipy/main.py @@ -0,0 +1,52 @@ +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import os + +from flask import Flask +import scipy.misc + +app = Flask(__name__) + + +# [START scipy] +@app.route('/') +def resize(): + """Demonstrates using scipy to resize an image.""" + app_path = os.path.dirname(os.path.realpath(__file__)) + image_path = os.path.join(app_path, 'assets/google_logo.jpg') + img = scipy.misc.imread(image_path) + img_tinted = scipy.misc.imresize(img, (300, 300)) + output_image_path = os.path.join( + app_path, 'assets/resized_google_logo.jpg') + # Write the tinted image back to disk + scipy.misc.imsave(output_image_path, img_tinted) + return "Image resized." +# [END scipy] + + +@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/scipy/main_test.py b/appengine/flexible/scipy/main_test.py new file mode 100644 index 00000000000..dd67baefeac --- /dev/null +++ b/appengine/flexible/scipy/main_test.py @@ -0,0 +1,35 @@ +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +import main + + +def test_index(): + main.app.testing = True + client = main.app.test_client() + test_path = os.path.dirname(os.path.realpath(__file__)) + asset_path = os.path.join( + test_path, 'assets/resized_google_logo.jpg') + fixtured_path = os.path.join( + test_path, 'fixtures/assets/resized_google_logo.jpg') + try: + os.remove(asset_path) + except OSError: + pass # if doesn't exist + r = client.get('/') + + assert os.path.isfile(fixtured_path) + assert r.status_code == 200 diff --git a/appengine/flexible/scipy/requirements.txt b/appengine/flexible/scipy/requirements.txt new file mode 100644 index 00000000000..3f96e4e48a7 --- /dev/null +++ b/appengine/flexible/scipy/requirements.txt @@ -0,0 +1,5 @@ +Flask==0.12.2 +gunicorn==19.7.1 +numpy==1.13.0 +scipy==0.19.1 +Pillow==4.1.1 diff --git a/appengine/flexible/sendgrid/README.md b/appengine/flexible/sendgrid/README.md new file mode 100644 index 00000000000..a709ae7ff05 --- /dev/null +++ b/appengine/flexible/sendgrid/README.md @@ -0,0 +1,24 @@ +# Python SendGrid email sample for Google App Engine Flexible + +This sample demonstrates how to use [SendGrid](https://www.sendgrid.com) on [Google App Engine Flexible](https://cloud.google.com/appengine). + +For more information about SendGrid, see their [documentation](https://sendgrid.com/docs/User_Guide/index.html). + +## Setup + +Before you can run or deploy the sample, you will need to do the following: + +1. [Create a SendGrid Account](http://sendgrid.com/partner/google). As of September 2015, Google users start with 25,000 free emails per month. + +2. Configure your SendGrid 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 and send emails from your local machine. You +will need to set environment variables before starting your application: + + $ export SENDGRID_API_KEY=[your-sendgrid-api-key] + $ export SENDGRID_SENDER=[your-sendgrid-sender-email-address] + $ python main.py diff --git a/appengine/flexible/sendgrid/app.yaml b/appengine/flexible/sendgrid/app.yaml new file mode 100644 index 00000000000..6161a615fef --- /dev/null +++ b/appengine/flexible/sendgrid/app.yaml @@ -0,0 +1,12 @@ +runtime: python +env: flex +entrypoint: gunicorn -b :$PORT main:app + +runtime_config: + python_version: 3 + +# [START env_variables] +env_variables: + SENDGRID_API_KEY: your-sendgrid-api-key + SENDGRID_SENDER: your-sendgrid-sender +# [END env_variables] diff --git a/appengine/flexible/sendgrid/main.py b/appengine/flexible/sendgrid/main.py new file mode 100644 index 00000000000..0cfcb51fd0c --- /dev/null +++ b/appengine/flexible/sendgrid/main.py @@ -0,0 +1,74 @@ +# Copyright 2015 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START app] +import logging +import os + +from flask import Flask, render_template, request +import sendgrid +from sendgrid.helpers import mail + +# [START config] +SENDGRID_API_KEY = os.environ['SENDGRID_API_KEY'] +SENDGRID_SENDER = os.environ['SENDGRID_SENDER'] +# [END config] + +app = Flask(__name__) + + +@app.route('/') +def index(): + return render_template('index.html') + + +# [START example] +@app.route('/send/email', methods=['POST']) +def send_email(): + to = request.form.get('to') + if not to: + return ('Please provide an email address in the "to" query string ' + 'parameter.'), 400 + + sg = sendgrid.SendGridAPIClient(apikey=SENDGRID_API_KEY) + + to_email = mail.Email(to) + from_email = mail.Email(SENDGRID_SENDER) + subject = 'This is a test email' + content = mail.Content('text/plain', 'Example message.') + message = mail.Mail(from_email, subject, to_email, content) + + response = sg.client.mail.send.post(request_body=message.get()) + + if response.status_code != 202: + return 'An error occurred: {}'.format(response.body), 500 + + return 'Email sent.' +# [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) +# [END app] diff --git a/appengine/flexible/sendgrid/main_test.py b/appengine/flexible/sendgrid/main_test.py new file mode 100644 index 00000000000..7577665f83f --- /dev/null +++ b/appengine/flexible/sendgrid/main_test.py @@ -0,0 +1,49 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import mock +import pytest + + +@pytest.fixture +def app(monkeypatch): + monkeypatch.setenv('SENDGRID_API_KEY', 'apikey') + monkeypatch.setenv('SENDGRID_SENDER', 'sender@example.com') + + import main + + main.app.testing = True + return main.app.test_client() + + +def test_get(app): + r = app.get('/') + assert r.status_code == 200 + + +@mock.patch('python_http_client.client.Client._make_request') +def test_post(make_request_mock, app): + response = mock.Mock() + response.getcode.return_value = 200 + response.read.return_value = 'OK' + response.info.return_value = {} + make_request_mock.return_value = response + + app.post('/send/email', data={ + 'to': 'user@example.com' + }) + + assert make_request_mock.called + request = make_request_mock.call_args[0][1] + assert 'user@example.com' in request.data.decode('utf-8') diff --git a/appengine/flexible/sendgrid/requirements.txt b/appengine/flexible/sendgrid/requirements.txt new file mode 100644 index 00000000000..aee0e236006 --- /dev/null +++ b/appengine/flexible/sendgrid/requirements.txt @@ -0,0 +1,3 @@ +Flask==0.12.2 +sendgrid==4.2.0 +gunicorn==19.7.1 diff --git a/appengine/flexible/sendgrid/templates/index.html b/appengine/flexible/sendgrid/templates/index.html new file mode 100644 index 00000000000..db095f71789 --- /dev/null +++ b/appengine/flexible/sendgrid/templates/index.html @@ -0,0 +1,29 @@ +{# +# Copyright 2015 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#} + + + + SendGrid on Google App Engine Flexible Environment + + + +
    + + +
    + + + diff --git a/appengine/flexible/static_files/README.md b/appengine/flexible/static_files/README.md new file mode 100644 index 00000000000..a2b38d0d633 --- /dev/null +++ b/appengine/flexible/static_files/README.md @@ -0,0 +1,7 @@ +# Python / Flask static files sample for Google App Engine Flexible Environment + +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..e5ac514e8b6 --- /dev/null +++ b/appengine/flexible/static_files/app.yaml @@ -0,0 +1,6 @@ +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 new file mode 100644 index 00000000000..ca218eb4975 --- /dev/null +++ b/appengine/flexible/static_files/main.py @@ -0,0 +1,42 @@ +# Copyright 2015 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START app] +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 app] diff --git a/appengine/flexible/static_files/main_test.py b/appengine/flexible/static_files/main_test.py new file mode 100644 index 00000000000..7fdd5fa92ae --- /dev/null +++ b/appengine/flexible/static_files/main_test.py @@ -0,0 +1,26 @@ +# Copyright 2015 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +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.txt b/appengine/flexible/static_files/requirements.txt new file mode 100644 index 00000000000..bb84e50e90f --- /dev/null +++ b/appengine/flexible/static_files/requirements.txt @@ -0,0 +1,2 @@ +Flask==0.12.2 +gunicorn==19.7.1 diff --git a/appengine/flexible/static_files/static/main.css b/appengine/flexible/static_files/static/main.css new file mode 100644 index 00000000000..fe0aca088c4 --- /dev/null +++ b/appengine/flexible/static_files/static/main.css @@ -0,0 +1,21 @@ +/* +Copyright 2015 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +/* [START example] */ +body { + font-family: Verdana, Helvetica, sans-serif; + background-color: #CCCCFF; +} +/* [END example] */ diff --git a/appengine/flexible/static_files/templates/index.html b/appengine/flexible/static_files/templates/index.html new file mode 100644 index 00000000000..352d00996f8 --- /dev/null +++ b/appengine/flexible/static_files/templates/index.html @@ -0,0 +1,29 @@ +{# +# Copyright 2015 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#} + + + + Static Files + + + + +

    This is a static file serving example.

    + + diff --git a/appengine/flexible/storage/README.md b/appengine/flexible/storage/README.md new file mode 100644 index 00000000000..3a38faa60ed --- /dev/null +++ b/appengine/flexible/storage/README.md @@ -0,0 +1,32 @@ +# Python Google Cloud Storage sample for Google App Engine Flexible Environment + +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/storage/app.yaml b/appengine/flexible/storage/app.yaml new file mode 100644 index 00000000000..46ac46e9900 --- /dev/null +++ b/appengine/flexible/storage/app.yaml @@ -0,0 +1,11 @@ +runtime: python +env: flex +entrypoint: gunicorn -b :$PORT main:app + +runtime_config: + python_version: 3 + +#[START env] +env_variables: + CLOUD_STORAGE_BUCKET: your-bucket-name +#[END env] diff --git a/appengine/flexible/storage/main.py b/appengine/flexible/storage/main.py new file mode 100644 index 00000000000..b5834362600 --- /dev/null +++ b/appengine/flexible/storage/main.py @@ -0,0 +1,84 @@ +# Copyright 2015 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START app] +import logging +import os + +from flask import Flask, request +from google.cloud import storage + +# [start config] +app = Flask(__name__) + + +# Configure this environment variable via app.yaml +CLOUD_STORAGE_BUCKET = os.environ['CLOUD_STORAGE_BUCKET'] +# [end config] + + +# [START form] +@app.route('/') +def index(): + return """ +
    + + +
    +""" +# [END form] + + +# [START upload] +@app.route('/upload', methods=['POST']) +def upload(): + """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 + ) + + # The public URL can be used to directly access the uploaded file via HTTP. + return blob.public_url +# [END upload] + + +@app.errorhandler(500) +def server_error(e): + logging.exception('An error occurred during a request.') + return """ + An internal error occurred:
    {}
    + See logs for full stacktrace. + """.format(e), 500 + + +if __name__ == '__main__': + # This is used when running locally. Gunicorn is used to run the + # application on Google App Engine. See entrypoint in app.yaml. + app.run(host='127.0.0.1', port=8080, debug=True) +# [END app] diff --git a/appengine/flexible/storage/main_test.py b/appengine/flexible/storage/main_test.py new file mode 100644 index 00000000000..76391d74f64 --- /dev/null +++ b/appengine/flexible/storage/main_test.py @@ -0,0 +1,50 @@ +# Copyright 2015 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest +import requests +from six import BytesIO + +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_upload(client): + # Upload a simple file + file_content = b"This is some test content." + + r = client.post( + '/upload', + data={ + 'file': (BytesIO(file_content), 'example.txt') + } + ) + + 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/storage/requirements.txt b/appengine/flexible/storage/requirements.txt new file mode 100644 index 00000000000..fe2ae8c00f5 --- /dev/null +++ b/appengine/flexible/storage/requirements.txt @@ -0,0 +1,3 @@ +Flask==0.12.2 +google-cloud-storage==1.2.0 +gunicorn==19.7.1 diff --git a/appengine/flexible/twilio/README.md b/appengine/flexible/twilio/README.md new file mode 100644 index 00000000000..b93af17be7c --- /dev/null +++ b/appengine/flexible/twilio/README.md @@ -0,0 +1,29 @@ +# Python Twilio voice and SMS sample for Google App Engine Flexible Environment + +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..bca1b610e00 --- /dev/null +++ b/appengine/flexible/twilio/app.yaml @@ -0,0 +1,13 @@ +runtime: python +env: flex +entrypoint: gunicorn -b :$PORT main:app + +runtime_config: + python_version: 3 + +# [START env_variables] +env_variables: + TWILIO_ACCOUNT_SID: your-account-sid + TWILIO_AUTH_TOKEN: your-auth-token + TWILIO_NUMBER: your-twilio-number +# [END env_variables] diff --git a/appengine/flexible/twilio/main.py b/appengine/flexible/twilio/main.py new file mode 100644 index 00000000000..e3fe8edd51a --- /dev/null +++ b/appengine/flexible/twilio/main.py @@ -0,0 +1,90 @@ +# Copyright 2015 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START app] +import logging +import os + +from flask import Flask, request +from twilio import twiml +from twilio.rest import TwilioRestClient + + +# [START configuration] +TWILIO_ACCOUNT_SID = os.environ['TWILIO_ACCOUNT_SID'] +TWILIO_AUTH_TOKEN = os.environ['TWILIO_AUTH_TOKEN'] +TWILIO_NUMBER = os.environ['TWILIO_NUMBER'] +# [END configuration] + + +app = Flask(__name__) + + +# [START receive_call] +@app.route('/call/receive', methods=['POST']) +def receive_call(): + """Answers a call and replies with a simple greeting.""" + response = twiml.Response() + response.say('Hello from Twilio!') + return str(response), 200, {'Content-Type': 'application/xml'} +# [END receive_call] + + +# [START 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 = TwilioRestClient(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN) + rv = client.messages.create( + to=to, + from_=TWILIO_NUMBER, + body='Hello from Twilio!') + return str(rv) +# [END send_sms] + + +# [START 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 = twiml.Response() + response.message(message) + return str(response), 200, {'Content-Type': 'application/xml'} +# [END 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) +# [END app] diff --git a/appengine/flexible/twilio/main_test.py b/appengine/flexible/twilio/main_test.py new file mode 100644 index 00000000000..b57c52d0d48 --- /dev/null +++ b/appengine/flexible/twilio/main_test.py @@ -0,0 +1,67 @@ +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json + +from googleapiclient.http import HttpMockSequence +import httplib2 +import pytest + + +class HttpMockSequenceWithCredentials(HttpMockSequence): + def add_credentials(self, *args): + pass + + +@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') + + +def test_send_sms(app, monkeypatch): + httpmock = HttpMockSequenceWithCredentials([ + ({'status': '200'}, json.dumps({ + 'sid': 'sid123' + })) + ]) + + def mock_http_ctor(*args, **kwargs): + return httpmock + + monkeypatch.setattr(httplib2, 'Http', mock_http_ctor) + + 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.txt b/appengine/flexible/twilio/requirements.txt new file mode 100644 index 00000000000..dda690ab2e6 --- /dev/null +++ b/appengine/flexible/twilio/requirements.txt @@ -0,0 +1,3 @@ +Flask==0.12.2 +gunicorn==19.7.1 +twilio<=6.0.0dev,==5.4.0 diff --git a/appengine/standard/README.md b/appengine/standard/README.md new file mode 100644 index 00000000000..c98838c11c1 --- /dev/null +++ b/appengine/standard/README.md @@ -0,0 +1,42 @@ +# Google App Engine Samples + +This section contains samples for [Google App Engine](https://cloud.google.com/appengine). Most of these samples have associated documentation that is linked +within the docstring of the sample itself. + +## Running the samples locally + +1. Download the [Google App Engine Python SDK](https://cloud.google.com/appengine/downloads) for your platform. +2. Many samples require extra libraries to be installed. If there is a `requirements.txt`, you will need to install the dependencies with [`pip`](pip.readthedocs.org). + + pip install -t lib -r requirements.txt + +3. Use `dev_appserver.py` to run the sample: + + dev_appserver.py app.yaml + +4. Visit `http://localhost:8080` to view your application. + +Some samples may require additional setup. Refer to individual sample READMEs. + +## Deploying the samples + +1. Download the [Google App Engine Python SDK](https://cloud.google.com/appengine/downloads) for your platform. +2. Many samples require extra libraries to be installed. If there is a `requirements.txt`, you will need to install the dependencies with [`pip`](pip.readthedocs.org). + + pip install -t lib -r requirements.txt + +3. Use `appcfg.py` to deploy the sample, you will need to specify your Project ID and a version number: + + appcfg.py update -A your-app-id -V your-version app.yaml + +4. Visit `https://your-app-id.appost.com` to view your application. + +## Additional resources + +For more information on App Engine: + +> https://cloud.google.com/appengine + +For more information on Python on App Engine: + +> https://cloud.google.com/appengine/docs/python diff --git a/appengine/standard/analytics/app.yaml b/appengine/standard/analytics/app.yaml new file mode 100644 index 00000000000..212e1aac4a8 --- /dev/null +++ b/appengine/standard/analytics/app.yaml @@ -0,0 +1,12 @@ +runtime: python27 +api_version: 1 +threadsafe: yes + +handlers: +- url: .* + script: main.app + +#[START env] +env_variables: + GA_TRACKING_ID: your-tracking-id +#[END env] diff --git a/appengine/standard/analytics/appengine_config.py b/appengine/standard/analytics/appengine_config.py new file mode 100644 index 00000000000..c903d9a0ac5 --- /dev/null +++ b/appengine/standard/analytics/appengine_config.py @@ -0,0 +1,18 @@ +# Copyright 2016 Google Inc. +# +# 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.appengine.ext import vendor + +# Add any libraries installed in the "lib" folder. +vendor.add('lib') diff --git a/appengine/standard/analytics/main.py b/appengine/standard/analytics/main.py new file mode 100644 index 00000000000..81cf8b7e53e --- /dev/null +++ b/appengine/standard/analytics/main.py @@ -0,0 +1,77 @@ +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import os + +from flask import Flask +import requests +import requests_toolbelt.adapters.appengine + +# Use the App Engine Requests adapter. This makes sure that Requests uses +# URLFetch. +requests_toolbelt.adapters.appengine.monkeypatch() + +app = Flask(__name__) + +# Environment variables are defined in app.yaml. +GA_TRACKING_ID = os.environ['GA_TRACKING_ID'] + + +# [START track_event] +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 + } + + response = requests.post( + 'http://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.' +# [END track_event] + + +@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/standard/analytics/main_test.py b/appengine/standard/analytics/main_test.py new file mode 100644 index 00000000000..db9d2a93691 --- /dev/null +++ b/appengine/standard/analytics/main_test.py @@ -0,0 +1,47 @@ +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import re + +import pytest +import responses + + +@pytest.fixture +def app(monkeypatch, testbed): + 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/standard/analytics/requirements.txt b/appengine/standard/analytics/requirements.txt new file mode 100644 index 00000000000..bd30ebf5941 --- /dev/null +++ b/appengine/standard/analytics/requirements.txt @@ -0,0 +1,3 @@ +Flask==0.12.2 +requests==2.18.1 +requests-toolbelt==0.8.0 diff --git a/appengine/standard/angular/README.md b/appengine/standard/angular/README.md new file mode 100644 index 00000000000..4e3c3539b85 --- /dev/null +++ b/appengine/standard/angular/README.md @@ -0,0 +1,5 @@ +## App Engine & Angular JS + +A simple [AngularJS](http://angularjs.org/) CRUD application for [Google App Engine](https://appengine.google.com/). + +Refer to the [App Engine Samples README](../README.md) for information on how to run and deploy this sample. diff --git a/appengine/standard/angular/app.yaml b/appengine/standard/angular/app.yaml new file mode 100644 index 00000000000..c7f82392acd --- /dev/null +++ b/appengine/standard/angular/app.yaml @@ -0,0 +1,15 @@ +runtime: python27 +threadsafe: true +api_version: 1 + +handlers: +- url: /rest/.* + script: main.APP + +- url: /(.+) + static_files: app/\1 + upload: app/.* + +- url: / + static_files: app/index.html + upload: app/index.html diff --git a/appengine/standard/angular/app/css/app.css b/appengine/standard/angular/app/css/app.css new file mode 100644 index 00000000000..fdcbacb1c12 --- /dev/null +++ b/appengine/standard/angular/app/css/app.css @@ -0,0 +1,5 @@ +.status { + color: blue; + padding: 1em; + height: 1em; +} diff --git a/appengine/standard/angular/app/index.html b/appengine/standard/angular/app/index.html new file mode 100644 index 00000000000..abb8b905885 --- /dev/null +++ b/appengine/standard/angular/app/index.html @@ -0,0 +1,14 @@ + + + + + + + + + +

    AngularJS Guest List

    +
    
    +    
    + + diff --git a/appengine/standard/angular/app/js/app.js b/appengine/standard/angular/app/js/app.js new file mode 100644 index 00000000000..2db33db4c21 --- /dev/null +++ b/appengine/standard/angular/app/js/app.js @@ -0,0 +1,123 @@ +'use strict'; + +var App = angular.module('App', ['ngRoute']); + +App.factory('myHttpInterceptor', function($rootScope, $q) { + return { + 'requestError': function(config) { + $rootScope.status = 'HTTP REQUEST ERROR ' + config; + return config || $q.when(config); + }, + 'responseError': function(rejection) { + $rootScope.status = 'HTTP RESPONSE ERROR ' + rejection.status + '\n' + + rejection.data; + return $q.reject(rejection); + }, + }; +}); + +App.factory('guestService', function($rootScope, $http, $q, $log) { + $rootScope.status = 'Retrieving data...'; + var deferred = $q.defer(); + $http.get('rest/query') + .success(function(data, status, headers, config) { + $rootScope.guests = data; + deferred.resolve(); + $rootScope.status = ''; + }); + return deferred.promise; +}); + +App.config(function($routeProvider) { + $routeProvider.when('/', { + controller : 'MainCtrl', + templateUrl: '/partials/main.html', + resolve : { 'guestService': 'guestService' }, + }); + $routeProvider.when('/invite', { + controller : 'InsertCtrl', + templateUrl: '/partials/insert.html', + }); + $routeProvider.when('/update/:id', { + controller : 'UpdateCtrl', + templateUrl: '/partials/update.html', + resolve : { 'guestService': 'guestService' }, + }); + $routeProvider.otherwise({ + redirectTo : '/' + }); +}); + +App.config(function($httpProvider) { + $httpProvider.interceptors.push('myHttpInterceptor'); +}); + +App.controller('MainCtrl', function($scope, $rootScope, $log, $http, $routeParams, $location, $route) { + + $scope.invite = function() { + $location.path('/invite'); + }; + + $scope.update = function(guest) { + $location.path('/update/' + guest.id); + }; + + $scope.delete = function(guest) { + $rootScope.status = 'Deleting guest ' + guest.id + '...'; + $http.post('/rest/delete', {'id': guest.id}) + .success(function(data, status, headers, config) { + for (var i=0; i<$rootScope.guests.length; i++) { + if ($rootScope.guests[i].id == guest.id) { + $rootScope.guests.splice(i, 1); + break; + } + } + $rootScope.status = ''; + }); + }; + +}); + +App.controller('InsertCtrl', function($scope, $rootScope, $log, $http, $routeParams, $location, $route) { + + $scope.submitInsert = function() { + var guest = { + first : $scope.first, + last : $scope.last, + }; + $rootScope.status = 'Creating...'; + $http.post('/rest/insert', guest) + .success(function(data, status, headers, config) { + $rootScope.guests.push(data); + $rootScope.status = ''; + }); + $location.path('/'); + } +}); + +App.controller('UpdateCtrl', function($routeParams, $rootScope, $scope, $log, $http, $location) { + + for (var i=0; i<$rootScope.guests.length; i++) { + if ($rootScope.guests[i].id == $routeParams.id) { + $scope.guest = angular.copy($rootScope.guests[i]); + } + } + + $scope.submitUpdate = function() { + $rootScope.status = 'Updating...'; + $http.post('/rest/update', $scope.guest) + .success(function(data, status, headers, config) { + for (var i=0; i<$rootScope.guests.length; i++) { + if ($rootScope.guests[i].id == $scope.guest.id) { + $rootScope.guests.splice(i,1); + break; + } + } + $rootScope.guests.push(data); + $rootScope.status = ''; + }); + $location.path('/'); + }; + +}); + diff --git a/appengine/standard/angular/app/partials/insert.html b/appengine/standard/angular/app/partials/insert.html new file mode 100644 index 00000000000..b4ed4c6c965 --- /dev/null +++ b/appengine/standard/angular/app/partials/insert.html @@ -0,0 +1,12 @@ +

    Invite another guest

    +
    +

    + + +

    +

    + + +

    + +
    diff --git a/appengine/standard/angular/app/partials/main.html b/appengine/standard/angular/app/partials/main.html new file mode 100644 index 00000000000..1d00341acc8 --- /dev/null +++ b/appengine/standard/angular/app/partials/main.html @@ -0,0 +1,7 @@ +

    Guest list

    + +
    + + + {{ $index + 1 }}. {{ guest.first }} {{ guest.last }} +
    diff --git a/appengine/standard/angular/app/partials/update.html b/appengine/standard/angular/app/partials/update.html new file mode 100644 index 00000000000..02711774231 --- /dev/null +++ b/appengine/standard/angular/app/partials/update.html @@ -0,0 +1,16 @@ +

    Update guest information

    +
    +

    + + +

    +

    + + +

    +

    + + +

    + +
    diff --git a/appengine/standard/angular/main.py b/appengine/standard/angular/main.py new file mode 100644 index 00000000000..127605e7d21 --- /dev/null +++ b/appengine/standard/angular/main.py @@ -0,0 +1,75 @@ +# Copyright 2013 Google, Inc +# +# 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 json + +import webapp2 + +import model + + +def AsDict(guest): + return {'id': guest.key.id(), 'first': guest.first, 'last': guest.last} + + +class RestHandler(webapp2.RequestHandler): + + def dispatch(self): + # time.sleep(1) + super(RestHandler, self).dispatch() + + def SendJson(self, r): + self.response.headers['content-type'] = 'text/plain' + self.response.write(json.dumps(r)) + + +class QueryHandler(RestHandler): + + def get(self): + guests = model.AllGuests() + r = [AsDict(guest) for guest in guests] + self.SendJson(r) + + +class UpdateHandler(RestHandler): + + def post(self): + r = json.loads(self.request.body) + guest = model.UpdateGuest(r['id'], r['first'], r['last']) + r = AsDict(guest) + self.SendJson(r) + + +class InsertHandler(RestHandler): + + def post(self): + r = json.loads(self.request.body) + guest = model.InsertGuest(r['first'], r['last']) + r = AsDict(guest) + self.SendJson(r) + + +class DeleteHandler(RestHandler): + + def post(self): + r = json.loads(self.request.body) + model.DeleteGuest(r['id']) + + +APP = webapp2.WSGIApplication([ + ('/rest/query', QueryHandler), + ('/rest/insert', InsertHandler), + ('/rest/delete', DeleteHandler), + ('/rest/update', UpdateHandler), +], debug=True) diff --git a/appengine/standard/angular/model.py b/appengine/standard/angular/model.py new file mode 100644 index 00000000000..46d1b5aee12 --- /dev/null +++ b/appengine/standard/angular/model.py @@ -0,0 +1,41 @@ +# Copyright 2013 Google, Inc +# +# 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.appengine.ext import ndb + + +class Guest(ndb.Model): + first = ndb.StringProperty() + last = ndb.StringProperty() + + +def AllGuests(): + return Guest.query() + + +def UpdateGuest(id, first, last): + guest = Guest(id=id, first=first, last=last) + guest.put() + return guest + + +def InsertGuest(first, last): + guest = Guest(first=first, last=last) + guest.put() + return guest + + +def DeleteGuest(id): + key = ndb.Key(Guest, id) + key.delete() diff --git a/appengine/standard/angular/scripts/deploy.sh b/appengine/standard/angular/scripts/deploy.sh new file mode 100755 index 00000000000..93598293294 --- /dev/null +++ b/appengine/standard/angular/scripts/deploy.sh @@ -0,0 +1,72 @@ +#!/bin/bash +# +set -ue + +VERSION=$(git log -1 --pretty=format:%H) +if [ -n "$(git status --porcelain)" ] +then + VERSION="dirty-$VERSION" +fi + +git status +echo +echo -e "Hit [ENTER] to continue: \c" +read + +SCRIPTS_DIR=$( dirname $0 ) +ROOT_DIR=$( dirname $SCRIPTS_DIR ) + +APPCFG=$(which appcfg.py) \ + || (echo "ERROR: appcfg.py must be in your PATH"; exit 1) +while [ -L $APPCFG ] +do + APPCFG=$(readlink $APPCFG) +done + +BIN_DIR=$(dirname $APPCFG) + +if [ "$(basename $BIN_DIR)" == "bin" ] +then + SDK_HOME=$(dirname $BIN_DIR) + if [ -d $SDK_HOME/platform/google_appengine ] + then + SDK_HOME=$SDK_HOME/platform/google_appengine + fi +else + SDK_HOME=$BIN_DIR +fi + +function get_app_id() { + local app_id + app_id=$( cat $ROOT_DIR/app.yaml | egrep '^application:' | sed 's/application: *\([0-9a-z][-0-9a-z]*[0-9a-z]\).*/\1/' ) + while [ $# -gt 0 ] + do + if [ "$1" == "-A" ] + then + shift + app_id=$1 + elif [ "${1/=*/}" == "--application" ] + then + app_id=${1/--application=/} + fi + shift + done + echo $app_id +} + +function deploy() { + echo -e "\n*** Rolling back any pending updates (just in case) ***\n" + appcfg.py --oauth2 $* rollback . + + echo -e "\n*** DEPLOYING ***\n" + appcfg.py --oauth2 $* update -V $VERSION . + + echo -e "\n*** SETTING DEFAULT VERSION ***\n" + appcfg.py --oauth2 $* set_default_version -V $VERSION . +} + +APP_ID=$(get_app_id $*) +echo +echo "Using app id: $APP_ID" + +deploy $* -A $APP_ID diff --git a/appengine/standard/angular/scripts/run.sh b/appengine/standard/angular/scripts/run.sh new file mode 100755 index 00000000000..a0345f56ad4 --- /dev/null +++ b/appengine/standard/angular/scripts/run.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# +set -uex + +dev_appserver.py \ + --host 0.0.0.0 \ + --admin_host 127.0.0.1 \ + --skip_sdk_update_check yes \ + . $* diff --git a/appengine/standard/app_identity/asserting/app.yaml b/appengine/standard/app_identity/asserting/app.yaml new file mode 100644 index 00000000000..42ad35ed2a8 --- /dev/null +++ b/appengine/standard/app_identity/asserting/app.yaml @@ -0,0 +1,7 @@ +runtime: python27 +threadsafe: yes +api_version: 1 + +handlers: +- url: .* + script: main.app diff --git a/appengine/standard/app_identity/asserting/main.py b/appengine/standard/app_identity/asserting/main.py new file mode 100644 index 00000000000..00d76e7e097 --- /dev/null +++ b/appengine/standard/app_identity/asserting/main.py @@ -0,0 +1,62 @@ +# Copyright 2015 Google Inc. +# +# 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. + +""" +Sample Google App Engine application that demonstrates using the App Engine +identity API to generate an auth token. + +For more information about App Engine, see README.md under /appengine. +""" + +# [START all] +import json +import logging + +from google.appengine.api import app_identity +from google.appengine.api import urlfetch +import webapp2 + + +class MainPage(webapp2.RequestHandler): + def get(self): + auth_token, _ = app_identity.get_access_token( + 'https://www.googleapis.com/auth/cloud-platform') + logging.info( + 'Using token {} to represent identity {}'.format( + auth_token, app_identity.get_service_account_name())) + + response = urlfetch.fetch( + 'https://www.googleapis.com/storage/v1/b?project={}'.format( + app_identity.get_application_id()), + method=urlfetch.GET, + headers={ + 'Authorization': 'Bearer {}'.format(auth_token) + } + ) + + if response.status_code != 200: + raise Exception( + 'Call failed. Status code {}. Body {}'.format( + response.status_code, response.content)) + + result = json.loads(response.content) + self.response.headers['Content-Type'] = 'application/json' + self.response.write(json.dumps(result, indent=2)) + + +app = webapp2.WSGIApplication([ + ('/', MainPage) +], debug=True) + +# [END all] diff --git a/appengine/standard/app_identity/asserting/main_test.py b/appengine/standard/app_identity/asserting/main_test.py new file mode 100644 index 00000000000..66169aa2b82 --- /dev/null +++ b/appengine/standard/app_identity/asserting/main_test.py @@ -0,0 +1,32 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import mock +import webtest + +import main + + +def test_app(testbed): + app = webtest.TestApp(main.app) + + with mock.patch('main.urlfetch.fetch') as fetch_mock: + result_mock = mock.Mock() + result_mock.status_code = 200 + result_mock.content = '{}' + fetch_mock.return_value = result_mock + + response = app.get('/') + assert response.status_int == 200 + assert fetch_mock.called diff --git a/appengine/standard/app_identity/incoming/app.yaml b/appengine/standard/app_identity/incoming/app.yaml new file mode 100644 index 00000000000..42ad35ed2a8 --- /dev/null +++ b/appengine/standard/app_identity/incoming/app.yaml @@ -0,0 +1,7 @@ +runtime: python27 +threadsafe: yes +api_version: 1 + +handlers: +- url: .* + script: main.app diff --git a/appengine/standard/app_identity/incoming/main.py b/appengine/standard/app_identity/incoming/main.py new file mode 100644 index 00000000000..68705ccd134 --- /dev/null +++ b/appengine/standard/app_identity/incoming/main.py @@ -0,0 +1,46 @@ +# Copyright 2015 Google Inc. +# +# 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. + +""" +Sample Google App Engine application that demonstrates usage of the app +engine inbound app ID header. + +For more information about App Engine, see README.md under /appengine. +""" + +# [START all] +import webapp2 + + +class MainPage(webapp2.RequestHandler): + allowed_app_ids = [ + 'other-app-id', + 'other-app-id-2' + ] + + def get(self): + incoming_app_id = self.request.headers.get( + 'X-Appengine-Inbound-Appid', None) + + if incoming_app_id not in self.allowed_app_ids: + self.abort(403) + + self.response.write('This is a protected page.') + + +app = webapp2.WSGIApplication([ + ('/', MainPage) +], debug=True) + +# [END all] diff --git a/appengine/standard/app_identity/incoming/main_test.py b/appengine/standard/app_identity/incoming/main_test.py new file mode 100644 index 00000000000..38faaa3f015 --- /dev/null +++ b/appengine/standard/app_identity/incoming/main_test.py @@ -0,0 +1,28 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import webtest + +import main + + +def test_app(testbed): + app = webtest.TestApp(main.app) + + response = app.get('/', status=403) + + response = app.get('/', headers={ + 'X-Appengine-Inbound-Appid': 'other-app-id' + }) + assert response.status_int == 200 diff --git a/appengine/standard/app_identity/signing/app.yaml b/appengine/standard/app_identity/signing/app.yaml new file mode 100644 index 00000000000..9f51c8eff36 --- /dev/null +++ b/appengine/standard/app_identity/signing/app.yaml @@ -0,0 +1,11 @@ +runtime: python27 +threadsafe: yes +api_version: 1 + +handlers: +- url: .* + script: main.app + +libraries: +- name: pycrypto + version: latest diff --git a/appengine/standard/app_identity/signing/main.py b/appengine/standard/app_identity/signing/main.py new file mode 100644 index 00000000000..63fc7097b34 --- /dev/null +++ b/appengine/standard/app_identity/signing/main.py @@ -0,0 +1,84 @@ +# Copyright 2015 Google Inc. +# +# 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. + +""" +Sample Google App Engine application that demonstrates usage of the app +identity API to sign bytes and verify signatures. + +For more information about App Engine, see README.md under /appengine. +""" + +# [START all] + +import base64 + +from Crypto.Hash import SHA256 +from Crypto.PublicKey import RSA +from Crypto.Signature import PKCS1_v1_5 +from Crypto.Util.asn1 import DerSequence +from google.appengine.api import app_identity +import webapp2 + + +def verify_signature(data, signature, x509_certificate): + """Verifies a signature using the given x.509 public key certificate.""" + + # PyCrypto 2.6 doesn't support x.509 certificates directly, so we'll need + # to extract the public key from it manually. + # This code is based on https://github.com/google/oauth2client/blob/master + # /oauth2client/_pycrypto_crypt.py + pem_lines = x509_certificate.replace(b' ', b'').split() + cert_der = base64.urlsafe_b64decode(b''.join(pem_lines[1:-1])) + cert_seq = DerSequence() + cert_seq.decode(cert_der) + tbs_seq = DerSequence() + tbs_seq.decode(cert_seq[0]) + public_key = RSA.importKey(tbs_seq[6]) + + signer = PKCS1_v1_5.new(public_key) + digest = SHA256.new(data) + + return signer.verify(digest, signature) + + +def verify_signed_by_app(data, signature): + """Checks the signature and data against all currently valid certificates + for the application.""" + public_certificates = app_identity.get_public_certificates() + + for cert in public_certificates: + if verify_signature(data, signature, cert.x509_certificate_pem): + return True + + return False + + +class MainPage(webapp2.RequestHandler): + def get(self): + message = 'Hello, world!' + signing_key_name, signature = app_identity.sign_blob(message) + verified = verify_signed_by_app(message, signature) + + self.response.content_type = 'text/plain' + self.response.write('Message: {}\n'.format(message)) + self.response.write( + 'Signature: {}\n'.format(base64.b64encode(signature))) + self.response.write('Verified: {}\n'.format(verified)) + + +app = webapp2.WSGIApplication([ + ('/', MainPage) +], debug=True) + +# [END all] diff --git a/appengine/standard/app_identity/signing/main_test.py b/appengine/standard/app_identity/signing/main_test.py new file mode 100644 index 00000000000..4864f9a18db --- /dev/null +++ b/appengine/standard/app_identity/signing/main_test.py @@ -0,0 +1,24 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import webtest + +import main + + +def test_app(testbed): + app = webtest.TestApp(main.app) + response = app.get('/') + assert response.status_int == 200 + assert 'Verified: True' in response.text diff --git a/appengine/standard/appstats/app.yaml b/appengine/standard/appstats/app.yaml new file mode 100644 index 00000000000..eb89129d68e --- /dev/null +++ b/appengine/standard/appstats/app.yaml @@ -0,0 +1,10 @@ +runtime: python27 +api_version: 1 +threadsafe: yes + +builtins: +- appstats: on + +handlers: +- url: .* + script: main.app diff --git a/appengine/standard/appstats/appengine_config.py b/appengine/standard/appstats/appengine_config.py new file mode 100644 index 00000000000..4c7a268901c --- /dev/null +++ b/appengine/standard/appstats/appengine_config.py @@ -0,0 +1,20 @@ +# Copyright 2016 Google Inc. +# +# 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. + + +# The app engine runtime will call this function during instance startup. +def webapp_add_wsgi_middleware(app): + from google.appengine.ext.appstats import recording + app = recording.appstats_wsgi_middleware(app) + return app diff --git a/appengine/standard/appstats/main.py b/appengine/standard/appstats/main.py new file mode 100644 index 00000000000..06257a17782 --- /dev/null +++ b/appengine/standard/appstats/main.py @@ -0,0 +1,37 @@ +# Copyright 2016 Google Inc. +# +# 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. + +""" +Sample Google App Engine application that demonstrates using appstats. + +For more information about App Engine, see README.md under /appengine. +""" + +# [START all] +from google.appengine.api import memcache +import webapp2 + + +class MainPage(webapp2.RequestHandler): + def get(self): + # Perform some RPCs so that appstats can capture them. + memcache.set('example_key', 50) + value = memcache.get('example_key') + self.response.write('Value is: {}'.format(value)) + + +app = webapp2.WSGIApplication([ + ('/', MainPage) +], debug=True) +# [END all] diff --git a/appengine/standard/appstats/main_test.py b/appengine/standard/appstats/main_test.py new file mode 100644 index 00000000000..9cceda3d317 --- /dev/null +++ b/appengine/standard/appstats/main_test.py @@ -0,0 +1,22 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import webtest + +import main + + +def test_app(testbed): + app = webtest.TestApp(main.app) + app.get('/') diff --git a/appengine/standard/background/README.md b/appengine/standard/background/README.md new file mode 100644 index 00000000000..10d1cd0e77a --- /dev/null +++ b/appengine/standard/background/README.md @@ -0,0 +1,9 @@ +# Using Background Threads from Google App Engine + +This example shows how to use manual or basic scaling to start App Engine background threads. + +See the [documentation on modules](https://cloud.google.com/appengine/docs/python/modules/) for +more information. + +Your app.yaml configuration must specify scaling as either manual or basic. The default +automatic scaling does not allow background threads. diff --git a/appengine/standard/background/app.yaml b/appengine/standard/background/app.yaml new file mode 100644 index 00000000000..57cdb0bf57c --- /dev/null +++ b/appengine/standard/background/app.yaml @@ -0,0 +1,10 @@ +runtime: python27 +api_version: 1 +threadsafe: yes + +manual_scaling: + instances: 1 + +handlers: +- url: .* + script: main.app diff --git a/appengine/standard/background/main.py b/appengine/standard/background/main.py new file mode 100644 index 00000000000..7a1d7be5928 --- /dev/null +++ b/appengine/standard/background/main.py @@ -0,0 +1,86 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Sample application that demonstrates how to use the App Engine background +threads. + +app.yaml scaling must be set to manual or basic. +""" + +# [START background-imp] +from google.appengine.api import background_thread +# [END background-imp] + +import webapp2 + +val = 'Dog' + + +class MainHandler(webapp2.RequestHandler): + def get(self): + self.response.headers['Content-Type'] = 'text/plain' + self.response.write(str(val)) + + +class SetDogHandler(webapp2.RequestHandler): + """ Resets the global val to 'Dog'""" + + def get(self): + global val + val = 'Dog' + self.response.headers['Content-Type'] = 'text/plain' + self.response.write('Done') + + +class SetCatBackgroundHandler(webapp2.RequestHandler): + """ Demonstrates two ways to start new background threads + """ + + def get(self): + """ + Demonstrates using a background thread to change the global + val from 'Dog' to 'Cat' + + The auto GET parameter determines whether to start the thread + automatically or manually + """ + auto = self.request.get('auto') + + # [START background-start] + # sample function to run in a background thread + def change_val(arg): + global val + val = arg + + if auto: + # Start the new thread in one command + background_thread.start_new_background_thread(change_val, ['Cat']) + else: + # create a new thread and start it + t = background_thread.BackgroundThread( + target=change_val, args=['Cat']) + t.start() + # [END background-start] + + self.response.headers['Content-Type'] = 'text/plain' + self.response.write('Done') + + +app = webapp2.WSGIApplication([ + ('/', MainHandler), + ('/dog', SetDogHandler), + ('/cat', SetCatBackgroundHandler), +], debug=True) +# [END all] diff --git a/appengine/standard/background/main_test.py b/appengine/standard/background/main_test.py new file mode 100644 index 00000000000..aa3b78caac7 --- /dev/null +++ b/appengine/standard/background/main_test.py @@ -0,0 +1,57 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +from mock import patch +import pytest +import webtest + +import main + +PROJECT = os.environ['GCLOUD_PROJECT'] + + +@pytest.fixture +def app(testbed): + main.PROJECTID = PROJECT + return webtest.TestApp(main.app) + + +@patch("main.background_thread") +def test_background(thread, app): + app.get('/dog') + response = app.get('/') + assert response.status_int == 200 + assert response.body == 'Dog' + app.get('/cat') + # no stub for system so manually set + main.val = 'Cat' + response = app.get('/') + assert response.status_int == 200 + assert response.body == 'Cat' + + +@patch("main.background_thread") +def test_background_auto_start(thread, app): + app.get('/dog') + response = app.get('/') + assert response.status_int == 200 + assert response.body == 'Dog' + app.get('/cat?auto=True') + # no stub for system so manually set + main.val = 'Cat' + response = app.get('/') + assert response.status_int == 200 + assert response.body == 'Cat' diff --git a/appengine/standard/bigquery/.gitignore b/appengine/standard/bigquery/.gitignore new file mode 100644 index 00000000000..a65b41774ad --- /dev/null +++ b/appengine/standard/bigquery/.gitignore @@ -0,0 +1 @@ +lib diff --git a/appengine/standard/bigquery/README.md b/appengine/standard/bigquery/README.md new file mode 100644 index 00000000000..0dc4b7cdb56 --- /dev/null +++ b/appengine/standard/bigquery/README.md @@ -0,0 +1,20 @@ +# Google App Engine accessing BigQuery using OAuth2 + +This sample demonstrates [authenticating to BigQuery in App Engine using OAuth2](https://cloud.google.com/bigquery/authentication). + + +These samples are used on the following documentation pages: + +> +* https://cloud.google.com/bigquery/authentication +* https://cloud.google.com/monitoring/api/authentication + + + +Refer to the [App Engine Samples README](../README.md) for information on how to run and deploy this sample. + +## Setup + +1. You'll need a client id for your project. Follow [these instructions](https://cloud.google.com/bigquery/authentication#clientsecrets). Once you've downloaded the client's json secret copy it into the sample directory and rename it to `client_secrets.json`. + +2. Update `main.py` and replace `` with your project's ID. diff --git a/appengine/standard/bigquery/app.yaml b/appengine/standard/bigquery/app.yaml new file mode 100644 index 00000000000..6fde9bebf2f --- /dev/null +++ b/appengine/standard/bigquery/app.yaml @@ -0,0 +1,9 @@ +application: cloud-samples-tests +version: 1 +runtime: python27 +api_version: 1 +threadsafe: yes + +handlers: +- url: .* + script: main.app diff --git a/appengine/standard/bigquery/appengine_config.py b/appengine/standard/bigquery/appengine_config.py new file mode 100644 index 00000000000..4fdc5d2b60f --- /dev/null +++ b/appengine/standard/bigquery/appengine_config.py @@ -0,0 +1,4 @@ +from google.appengine.ext import vendor + +# Add any libraries installed in the "lib" folder. +vendor.add('lib') diff --git a/appengine/standard/bigquery/client_secrets.json b/appengine/standard/bigquery/client_secrets.json new file mode 100644 index 00000000000..767caf645e6 --- /dev/null +++ b/appengine/standard/bigquery/client_secrets.json @@ -0,0 +1,3 @@ +{"web":{ + "client_id":"NOTE: this is just a placeholder for unit tests. See the README for what to replace this file with.", + "auth_uri":"TODO","token_uri":"TODO","auth_provider_x509_cert_url":"TODO","client_email":"","client_x509_cert_url":"","client_secret":"TODO","redirect_uris":["TODO","TODO"],"javascript_origins":["TODO","TODO"]}} diff --git a/appengine/standard/bigquery/main.py b/appengine/standard/bigquery/main.py new file mode 100644 index 00000000000..4437bf4457d --- /dev/null +++ b/appengine/standard/bigquery/main.py @@ -0,0 +1,68 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START all] + +""" +Sample App Engine application that demonstrates authentication to BigQuery +using User OAuth2 as opposed to OAuth2 Service Accounts. + +For more information, see README.md. +""" + +import json +import os + +import googleapiclient.discovery +from oauth2client.contrib.appengine import OAuth2DecoratorFromClientSecrets +import webapp2 + + +# The project id whose datasets you'd like to list +PROJECTID = '' + +# Create the method decorator for oauth. +decorator = OAuth2DecoratorFromClientSecrets( + os.path.join(os.path.dirname(__file__), 'client_secrets.json'), + scope='https://www.googleapis.com/auth/bigquery') + +# Create the bigquery api client +service = googleapiclient.discovery.build('bigquery', 'v2') + + +class MainPage(webapp2.RequestHandler): + + # oauth_required ensures that the user goes through the OAuth2 + # authorization flow before reaching this handler. + @decorator.oauth_required + def get(self): + # This is an httplib2.Http instance that is signed with the user's + # credentials. This allows you to access the BigQuery API on behalf + # of the user. + http = decorator.http() + + response = service.datasets().list(projectId=PROJECTID).execute(http) + + self.response.out.write('

    Datasets.list raw response:

    ') + self.response.out.write('
    %s
    ' % + json.dumps(response, sort_keys=True, indent=4, + separators=(',', ': '))) + + +app = webapp2.WSGIApplication([ + ('/', MainPage), + # Create the endpoint to receive oauth flow callbacks + (decorator.callback_path, decorator.callback_handler()) +], debug=True) +# [END all] diff --git a/appengine/standard/bigquery/main_test.py b/appengine/standard/bigquery/main_test.py new file mode 100644 index 00000000000..443d30b6dee --- /dev/null +++ b/appengine/standard/bigquery/main_test.py @@ -0,0 +1,69 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import re + +from googleapiclient.http import HttpMock +import mock +import pytest +import webtest + +import main + +RESOURCES = os.path.join(os.path.dirname(__file__), 'resources') +PROJECT = os.environ['GCLOUD_PROJECT'] + + +@pytest.fixture +def app(testbed): + main.PROJECTID = PROJECT + return webtest.TestApp(main.app) + + +def test_anonymous(app): + response = app.get('/') + + # Should redirect to login + assert response.status_int == 302 + assert re.search(r'.*accounts.*Login.*', response.headers['Location']) + + +def test_loggedin(app, login): + login() + + response = app.get('/') + + # Should redirect to oauth2 + assert response.status_int == 302 + assert re.search(r'.*oauth2.*', response.headers['Location']) + + +def test_oauthed(app, login): + login() + + mock_http = HttpMock( + os.path.join(RESOURCES, 'datasets-list.json'), + {'status': '200'}) + + with mock.patch.object(main.decorator, 'http', return_value=mock_http): + with mock.patch.object( + main.decorator, 'has_credentials', return_value=True): + response = app.get('/') + + # Should make the api call + assert response.status_int == 200 + assert re.search( + re.compile(r'.*datasets.*datasetReference.*etag.*', re.DOTALL), + response.body) diff --git a/appengine/standard/bigquery/requirements.txt b/appengine/standard/bigquery/requirements.txt new file mode 100644 index 00000000000..4f77d6936d7 --- /dev/null +++ b/appengine/standard/bigquery/requirements.txt @@ -0,0 +1 @@ +google-api-python-client==1.6.2 diff --git a/appengine/standard/bigquery/resources/datasets-list.json b/appengine/standard/bigquery/resources/datasets-list.json new file mode 100644 index 00000000000..c9a3877c4db --- /dev/null +++ b/appengine/standard/bigquery/resources/datasets-list.json @@ -0,0 +1,14 @@ +{ + "datasets": [ + { + "datasetReference": { + "datasetId": "test_dataset_java", + "projectId": "cloud-samples-tests" + }, + "id": "cloud-samples-tests:test_dataset_java", + "kind": "bigquery#dataset" + } + ], + "etag": "\"ZduQht1tG1odVP6IPm66xfuN2eI/HmGRlylAN_zCB6N4JDeX_XDO0R0\"", + "kind": "bigquery#datasetList" +} diff --git a/appengine/standard/blobstore/api/README.md b/appengine/standard/blobstore/api/README.md new file mode 100644 index 00000000000..0f7531d4848 --- /dev/null +++ b/appengine/standard/blobstore/api/README.md @@ -0,0 +1,11 @@ +# App Engine Blobstore Sample + + +These samples are used on the following documentation pages: + +> +* https://cloud.google.com/appengine/docs/python/tools/webapp/blobstorehandlers +* https://cloud.google.com/appengine/docs/python/blobstore/ + + + --> diff --git a/appengine/standard/blobstore/api/app.yaml b/appengine/standard/blobstore/api/app.yaml new file mode 100644 index 00000000000..636a04bae7e --- /dev/null +++ b/appengine/standard/blobstore/api/app.yaml @@ -0,0 +1,8 @@ +runtime: python27 +api_version: 1 +threadsafe: yes + +handlers: +- url: .* + script: main.app + login: required diff --git a/appengine/standard/blobstore/api/main.py b/appengine/standard/blobstore/api/main.py new file mode 100644 index 00000000000..841cbf1701d --- /dev/null +++ b/appengine/standard/blobstore/api/main.py @@ -0,0 +1,85 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Sample application that demonstrates how to use the App Engine Blobstore API. + +For more information, see README.md. +""" + +# [START all] +from google.appengine.api import users +from google.appengine.ext import blobstore +from google.appengine.ext import ndb +from google.appengine.ext.webapp import blobstore_handlers +import webapp2 + + +# This datastore model keeps track of which users uploaded which photos. +class UserPhoto(ndb.Model): + user = ndb.StringProperty() + blob_key = ndb.BlobKeyProperty() + + +class PhotoUploadFormHandler(webapp2.RequestHandler): + def get(self): + # [START upload_url] + upload_url = blobstore.create_upload_url('/upload_photo') + # [END upload_url] + # [START upload_form] + # To upload files to the blobstore, the request method must be "POST" + # and enctype must be set to "multipart/form-data". + self.response.out.write(""" + +
    + Upload File:
    + +
    +""".format(upload_url)) + # [END upload_form] + + +# [START upload_handler] +class PhotoUploadHandler(blobstore_handlers.BlobstoreUploadHandler): + def post(self): + try: + upload = self.get_uploads()[0] + user_photo = UserPhoto( + user=users.get_current_user().user_id(), + blob_key=upload.key()) + user_photo.put() + + self.redirect('/view_photo/%s' % upload.key()) + + except: + self.error(500) +# [END upload_handler] + + +# [START download_handler] +class ViewPhotoHandler(blobstore_handlers.BlobstoreDownloadHandler): + def get(self, photo_key): + if not blobstore.get(photo_key): + self.error(404) + else: + self.send_blob(photo_key) +# [END download_handler] + + +app = webapp2.WSGIApplication([ + ('/', PhotoUploadFormHandler), + ('/upload_photo', PhotoUploadHandler), + ('/view_photo/([^/]+)?', ViewPhotoHandler), +], debug=True) +# [END all] diff --git a/appengine/standard/blobstore/api/main_test.py b/appengine/standard/blobstore/api/main_test.py new file mode 100644 index 00000000000..adfc579698e --- /dev/null +++ b/appengine/standard/blobstore/api/main_test.py @@ -0,0 +1,26 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import webtest + +import main + + +def test_app(testbed, login): + app = webtest.TestApp(main.app) + + login() + response = app.get('/') + + assert '/_ah/upload' in response diff --git a/appengine/standard/blobstore/blobreader/app.yaml b/appengine/standard/blobstore/blobreader/app.yaml new file mode 100644 index 00000000000..636a04bae7e --- /dev/null +++ b/appengine/standard/blobstore/blobreader/app.yaml @@ -0,0 +1,8 @@ +runtime: python27 +api_version: 1 +threadsafe: yes + +handlers: +- url: .* + script: main.app + login: required diff --git a/appengine/standard/blobstore/blobreader/appengine_config.py b/appengine/standard/blobstore/blobreader/appengine_config.py new file mode 100644 index 00000000000..c903d9a0ac5 --- /dev/null +++ b/appengine/standard/blobstore/blobreader/appengine_config.py @@ -0,0 +1,18 @@ +# Copyright 2016 Google Inc. +# +# 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.appengine.ext import vendor + +# Add any libraries installed in the "lib" folder. +vendor.add('lib') diff --git a/appengine/standard/blobstore/blobreader/main.py b/appengine/standard/blobstore/blobreader/main.py new file mode 100644 index 00000000000..b9e2c5f4467 --- /dev/null +++ b/appengine/standard/blobstore/blobreader/main.py @@ -0,0 +1,69 @@ +"""A sample app that operates on GCS files with blobstore API's BlobReader.""" + +import cloudstorage +from google.appengine.api import app_identity +from google.appengine.ext import blobstore +import webapp2 + + +class BlobreaderHandler(webapp2.RequestHandler): + def get(self): + # Get the default Cloud Storage Bucket name and create a file name for + # the object in Cloud Storage. + bucket = app_identity.get_default_gcs_bucket_name() + + # Cloud Storage file names are in the format /bucket/object. + filename = '/{}/blobreader_demo'.format(bucket) + + # Create a file in Google Cloud Storage and write something to it. + with cloudstorage.open(filename, 'w') as filehandle: + filehandle.write('abcde\n') + + # In order to read the contents of the file using the Blobstore API, + # you must create a blob_key from the Cloud Storage file name. + # Blobstore expects the filename to be in the format of: + # /gs/bucket/object + blobstore_filename = '/gs{}'.format(filename) + blob_key = blobstore.create_gs_key(blobstore_filename) + + # [START blob_reader] + # Instantiate a BlobReader for a given Blobstore blob_key. + blob_reader = blobstore.BlobReader(blob_key) + + # Instantiate a BlobReader for a given Blobstore blob_key, setting the + # buffer size to 1 MB. + blob_reader = blobstore.BlobReader(blob_key, buffer_size=1048576) + + # Instantiate a BlobReader for a given Blobstore blob_key, setting the + # initial read position. + blob_reader = blobstore.BlobReader(blob_key, position=0) + + # Read the entire value into memory. This may take a while depending + # on the size of the value and the size of the read buffer, and is not + # recommended for large values. + blob_reader_data = blob_reader.read() + + # Write the contents to the response. + self.response.headers['Content-Type'] = 'text/plain' + self.response.write(blob_reader_data) + + # Set the read position back to 0, then read and write 3 bytes. + blob_reader.seek(0) + blob_reader_data = blob_reader.read(3) + self.response.write(blob_reader_data) + self.response.write('\n') + + # Set the read position back to 0, then read and write one line (up to + # and including a '\n' character) at a time. + blob_reader.seek(0) + for line in blob_reader: + self.response.write(line) + # [END blob_reader] + + # Delete the file from Google Cloud Storage using the blob_key. + blobstore.delete(blob_key) + + +app = webapp2.WSGIApplication([ + ('/', BlobreaderHandler), + ('/blobreader', BlobreaderHandler)], debug=True) diff --git a/appengine/standard/blobstore/blobreader/main_test.py b/appengine/standard/blobstore/blobreader/main_test.py new file mode 100644 index 00000000000..b0cc5898a42 --- /dev/null +++ b/appengine/standard/blobstore/blobreader/main_test.py @@ -0,0 +1,25 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import webtest + +import main + + +def test_blobreader(testbed, login): + app = webtest.TestApp(main.app) + + response = app.get('/blobreader') + + assert 'abcde\nabc\nabcde\n' in response diff --git a/appengine/standard/blobstore/blobreader/requirements.txt b/appengine/standard/blobstore/blobreader/requirements.txt new file mode 100644 index 00000000000..f2ec35f05f9 --- /dev/null +++ b/appengine/standard/blobstore/blobreader/requirements.txt @@ -0,0 +1 @@ +GoogleAppEngineCloudStorageClient==1.9.22.1 diff --git a/appengine/standard/blobstore/gcs/app.yaml b/appengine/standard/blobstore/gcs/app.yaml new file mode 100644 index 00000000000..636a04bae7e --- /dev/null +++ b/appengine/standard/blobstore/gcs/app.yaml @@ -0,0 +1,8 @@ +runtime: python27 +api_version: 1 +threadsafe: yes + +handlers: +- url: .* + script: main.app + login: required diff --git a/appengine/standard/blobstore/gcs/appengine_config.py b/appengine/standard/blobstore/gcs/appengine_config.py new file mode 100644 index 00000000000..c903d9a0ac5 --- /dev/null +++ b/appengine/standard/blobstore/gcs/appengine_config.py @@ -0,0 +1,18 @@ +# Copyright 2016 Google Inc. +# +# 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.appengine.ext import vendor + +# Add any libraries installed in the "lib" folder. +vendor.add('lib') diff --git a/appengine/standard/blobstore/gcs/main.py b/appengine/standard/blobstore/gcs/main.py new file mode 100644 index 00000000000..4ba0816ad90 --- /dev/null +++ b/appengine/standard/blobstore/gcs/main.py @@ -0,0 +1,76 @@ +"""A sample app that operates on GCS files with blobstore API.""" + +import cloudstorage +from google.appengine.api import app_identity +from google.appengine.ext import blobstore +from google.appengine.ext.webapp import blobstore_handlers +import webapp2 + + +# This handler creates a file in Cloud Storage using the cloudstorage +# client library and then reads the data back using the Blobstore API. +class CreateAndReadFileHandler(webapp2.RequestHandler): + def get(self): + # Get the default Cloud Storage Bucket name and create a file name for + # the object in Cloud Storage. + bucket = app_identity.get_default_gcs_bucket_name() + + # Cloud Storage file names are in the format /bucket/object. + filename = '/{}/blobstore_demo'.format(bucket) + + # Create a file in Google Cloud Storage and write something to it. + with cloudstorage.open(filename, 'w') as filehandle: + filehandle.write('abcde\n') + + # In order to read the contents of the file using the Blobstore API, + # you must create a blob_key from the Cloud Storage file name. + # Blobstore expects the filename to be in the format of: + # /gs/bucket/object + blobstore_filename = '/gs{}'.format(filename) + blob_key = blobstore.create_gs_key(blobstore_filename) + + # Read the file's contents using the Blobstore API. + # The last two parameters specify the start and end index of bytes we + # want to read. + data = blobstore.fetch_data(blob_key, 0, 6) + + # Write the contents to the response. + self.response.headers['Content-Type'] = 'text/plain' + self.response.write(data) + + # Delete the file from Google Cloud Storage using the blob_key. + blobstore.delete(blob_key) + + +# This handler creates a file in Cloud Storage using the cloudstorage +# client library and then serves the file back using the Blobstore API. +class CreateAndServeFileHandler(blobstore_handlers.BlobstoreDownloadHandler): + + def get(self): + # Get the default Cloud Storage Bucket name and create a file name for + # the object in Cloud Storage. + bucket = app_identity.get_default_gcs_bucket_name() + + # Cloud Storage file names are in the format /bucket/object. + filename = '/{}/blobstore_serving_demo'.format(bucket) + + # Create a file in Google Cloud Storage and write something to it. + with cloudstorage.open(filename, 'w') as filehandle: + filehandle.write('abcde\n') + + # In order to read the contents of the file using the Blobstore API, + # you must create a blob_key from the Cloud Storage file name. + # Blobstore expects the filename to be in the format of: + # /gs/bucket/object + blobstore_filename = '/gs{}'.format(filename) + blob_key = blobstore.create_gs_key(blobstore_filename) + + # BlobstoreDownloadHandler serves the file from Google Cloud Storage to + # your computer using blob_key. + self.send_blob(blob_key) + + +app = webapp2.WSGIApplication([ + ('/', CreateAndReadFileHandler), + ('/blobstore/read', CreateAndReadFileHandler), + ('/blobstore/serve', CreateAndServeFileHandler)], debug=True) diff --git a/appengine/standard/blobstore/gcs/main_test.py b/appengine/standard/blobstore/gcs/main_test.py new file mode 100644 index 00000000000..b5d1c27eb32 --- /dev/null +++ b/appengine/standard/blobstore/gcs/main_test.py @@ -0,0 +1,34 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import webtest + +import main + + +def test_create_and_read(testbed, login): + app = webtest.TestApp(main.app) + + response = app.get('/blobstore/read') + + assert 'abcde' in response + + +def test_create_and_serve(testbed, login): + app = webtest.TestApp(main.app) + + response = app.get('/blobstore/serve') + served_file_header = response.headers['X-AppEngine-BlobKey'] + + assert 'encoded_gs_file' in served_file_header diff --git a/appengine/standard/blobstore/gcs/requirements.txt b/appengine/standard/blobstore/gcs/requirements.txt new file mode 100644 index 00000000000..f2ec35f05f9 --- /dev/null +++ b/appengine/standard/blobstore/gcs/requirements.txt @@ -0,0 +1 @@ +GoogleAppEngineCloudStorageClient==1.9.22.1 diff --git a/appengine/standard/channel/README.md b/appengine/standard/channel/README.md new file mode 100644 index 00000000000..dc402e1b721 --- /dev/null +++ b/appengine/standard/channel/README.md @@ -0,0 +1,4 @@ +To deploy, run the following command, or see [here](https://cloud.google.com/appengine/docs/python/gettingstartedpython27/uploading) for more details about uploading your application. + +`appcfg.py -A update .` + diff --git a/appengine/standard/channel/app.yaml b/appengine/standard/channel/app.yaml new file mode 100644 index 00000000000..b56bf2c3c0d --- /dev/null +++ b/appengine/standard/channel/app.yaml @@ -0,0 +1,15 @@ +module: tictactoe +runtime: python27 +version: 1 +api_version: 1 +threadsafe: true + +handlers: +- url: /.* + script: chatactoe.app + +libraries: +- name: webapp2 + version: "2.5.2" +- name: jinja2 + version: "2.6" diff --git a/appengine/standard/channel/chatactoe.py b/appengine/standard/channel/chatactoe.py new file mode 100644 index 00000000000..b0514325481 --- /dev/null +++ b/appengine/standard/channel/chatactoe.py @@ -0,0 +1,204 @@ +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Channel Tic Tac Toe +This module demonstrates the App Engine Channel API by implementing a +simple tic-tac-toe game. +For more information, see the README.md. +""" + +# [START all] +import json +import os +import re + +from google.appengine.api import app_identity +from google.appengine.api import channel +from google.appengine.api import users +from google.appengine.ext import db +import jinja2 +import webapp2 + +CLOUD_PROJECT_ID = app_identity.get_application_id() + + +class Game(db.Model): + """All the data we store for a game""" + userX = db.UserProperty() + userO = db.UserProperty() + board = db.StringProperty() + moveX = db.BooleanProperty() + winner = db.StringProperty() + winning_board = db.StringProperty() + + +class Wins(): + x_win_patterns = ['XXX......', '...XXX...', '......XXX', 'X..X..X..', + '.X..X..X.', '..X..X..X', 'X...X...X', '..X.X.X..'] + + o_win_patterns = map(lambda s: s.replace('X', 'O'), x_win_patterns) + + x_wins = map(lambda s: re.compile(s), x_win_patterns) + o_wins = map(lambda s: re.compile(s), o_win_patterns) + + +# [START validate_message_3] +class GameUpdater(): + """Creates an object to store the game's state, and handles validating moves + and broadcasting updates to the game.""" + game = None + + def __init__(self, game): + self.game = game + + def get_game_message(self): + gameUpdate = { + 'board': self.game.board, + 'userX': self.game.userX.user_id(), + 'userO': '' if not self.game.userO else self.game.userO.user_id(), + 'moveX': self.game.moveX, + 'winner': self.game.winner, + 'winningBoard': self.game.winning_board + } + return json.dumps(gameUpdate) + + def send_update(self): + message = self.get_game_message() + channel.send_message( + self.game.userX.user_id() + self.game.key().id_or_name(), message) + if self.game.userO: + channel.send_message(self.game.userO.user_id() + + self.game.key().id_or_name(), message) + + def check_win(self): + if self.game.moveX: + # O just moved, check for O wins + wins = Wins().o_wins + potential_winner = self.game.userO.user_id() + else: + # X just moved, check for X wins + wins = Wins().x_wins + potential_winner = self.game.userX.user_id() + + for win in wins: + if win.match(self.game.board): + self.game.winner = potential_winner + self.game.winning_board = win.pattern + return + + def make_move(self, position, user): + if (position >= 0 and user == self.game.userX or + user == self.game.userO): + if self.game.moveX == (user == self.game.userX): + boardList = list(self.game.board) + if (boardList[position] == ' '): + boardList[position] = 'X' if self.game.moveX else 'O' + self.game.board = "".join(boardList) + self.game.moveX = not self.game.moveX + self.check_win() + self.game.put() + self.send_update() + return +# [END validate_message_3] + + +# [START validate_message_2] +class GameFromRequest(): + game = None + + def __init__(self, request): + user = users.get_current_user() + game_key = request.get('g') + if user and game_key: + self.game = Game.get_by_key_name(game_key) + + def get_game(self): + return self.game +# [END validate_message_2] + + +# [START validate_message_1] +class MovePage(webapp2.RequestHandler): + def post(self): + game = GameFromRequest(self.request).get_game() + user = users.get_current_user() + if game and user: + id = int(self.request.get('i')) + GameUpdater(game).make_move(id, user) +# [END validate_message_1] + + +class OpenedPage(webapp2.RequestHandler): + def post(self): + game = GameFromRequest(self.request).get_game() + GameUpdater(game).send_update() + + +# [START create_channel_1] +class MainPage(webapp2.RequestHandler): + """The main UI page, renders the 'index.html' template.""" + + def get(self): + """Renders the main page. When this page is shown, we create a new + channel to push asynchronous updates to the client.""" + user = users.get_current_user() + game_key = self.request.get('g') + game = None + if user: + if not game_key: + game_key = user.user_id() + game = Game(key_name=game_key, + userX=user, + moveX=True, + board=' ') + game.put() + else: + game = Game.get_by_key_name(game_key) + # if not game.userO: + if not game.userO and game.userX != user: + game.userO = user + game.put() + + global CLOUD_PROJECT_ID + game_link = ( + 'https://' + CLOUD_PROJECT_ID + '.appspot.com/?g=' + game_key) + + if game: + token = channel.create_channel(user.user_id() + game_key) + template_values = {'token': token, + 'me': user.user_id(), + 'game_key': game_key, + 'game_link': game_link, + 'initial_message': + GameUpdater(game).get_game_message()} + template = jinja_environment.get_template('index.html') + self.response.out.write(template.render(template_values)) + else: + self.response.out.write('No such game') + else: + self.redirect(users.create_login_url(self.request.uri)) +# [END create_channel_1] + + +jinja_environment = jinja2.Environment( + loader=jinja2.FileSystemLoader(os.path.dirname(__file__))) + +app = webapp2.WSGIApplication([ + ('/', MainPage), + ('/opened', OpenedPage), + ('/move', MovePage) +], debug=True) + +# [END all] diff --git a/appengine/standard/channel/index.html b/appengine/standard/channel/index.html new file mode 100644 index 00000000000..df5b0c631d6 --- /dev/null +++ b/appengine/standard/channel/index.html @@ -0,0 +1,259 @@ + + + + + + + +
    +

    Channel-based Tic Tac Toe

    + + + +
    + You won this game! +
    +
    + You lost this game. +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + Quick link to this game: {{ game_link }} +
    +
    + + diff --git a/appengine/standard/cloudsql/README.md b/appengine/standard/cloudsql/README.md new file mode 100644 index 00000000000..e4d839cb2ac --- /dev/null +++ b/appengine/standard/cloudsql/README.md @@ -0,0 +1,11 @@ +# Using Cloud SQL from Google App Engine + +This is an example program showing how to use the native MySQL connections from Google App Engine to [Google Cloud SQL](https://cloud.google.com/sql). + +Refer to the [App Engine Samples README](../README.md) for information on how to run and deploy this sample. + +## Setup + +1. You will need to create a [Cloud SQL instance](https://cloud.google.com/sql/docs/create-instance). + +2. Edit the update the `env_variables` section in `app.yaml` with your Cloud SQL configuration. diff --git a/appengine/standard/cloudsql/app.yaml b/appengine/standard/cloudsql/app.yaml new file mode 100644 index 00000000000..22df6a2dce0 --- /dev/null +++ b/appengine/standard/cloudsql/app.yaml @@ -0,0 +1,18 @@ +runtime: python27 +api_version: 1 +threadsafe: yes + +handlers: +- url: / + script: main.app + +libraries: +- name: MySQLdb + version: "latest" + +# [START env_variables] +env_variables: + CLOUDSQL_CONNECTION_NAME: your-connection-name + CLOUDSQL_USER: root + CLOUDSQL_PASSWORD: your-cloudsql-user-password +# [END env_variables] diff --git a/appengine/standard/cloudsql/main.py b/appengine/standard/cloudsql/main.py new file mode 100644 index 00000000000..a0d5f109ec6 --- /dev/null +++ b/appengine/standard/cloudsql/main.py @@ -0,0 +1,79 @@ +# Copyright 2013 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Sample App Engine application demonstrating how to connect to Google Cloud SQL +using App Engine's native unix socket or using TCP when running locally. + +For more information, see the README.md. +""" + +# [START all] +import os + +import MySQLdb +import webapp2 + + +# These environment variables are configured in app.yaml. +CLOUDSQL_CONNECTION_NAME = os.environ.get('CLOUDSQL_CONNECTION_NAME') +CLOUDSQL_USER = os.environ.get('CLOUDSQL_USER') +CLOUDSQL_PASSWORD = os.environ.get('CLOUDSQL_PASSWORD') + + +def connect_to_cloudsql(): + # When deployed to App Engine, the `SERVER_SOFTWARE` environment variable + # will be set to 'Google App Engine/version'. + if os.getenv('SERVER_SOFTWARE', '').startswith('Google App Engine/'): + # Connect using the unix socket located at + # /cloudsql/cloudsql-connection-name. + cloudsql_unix_socket = os.path.join( + '/cloudsql', CLOUDSQL_CONNECTION_NAME) + + db = MySQLdb.connect( + unix_socket=cloudsql_unix_socket, + user=CLOUDSQL_USER, + passwd=CLOUDSQL_PASSWORD) + + # If the unix socket is unavailable, then try to connect using TCP. This + # will work if you're running a local MySQL server or using the Cloud SQL + # proxy, for example: + # + # $ cloud_sql_proxy -instances=your-connection-name=tcp:3306 + # + else: + db = MySQLdb.connect( + host='127.0.0.1', user=CLOUDSQL_USER, passwd=CLOUDSQL_PASSWORD) + + return db + + +class MainPage(webapp2.RequestHandler): + def get(self): + """Simple request handler that shows all of the MySQL variables.""" + self.response.headers['Content-Type'] = 'text/plain' + + db = connect_to_cloudsql() + cursor = db.cursor() + cursor.execute('SHOW VARIABLES') + + for r in cursor.fetchall(): + self.response.write('{}\n'.format(r)) + + +app = webapp2.WSGIApplication([ + ('/', MainPage), +], debug=True) + +# [END all] diff --git a/appengine/standard/cloudsql/main_test.py b/appengine/standard/cloudsql/main_test.py new file mode 100644 index 00000000000..d20847d02c4 --- /dev/null +++ b/appengine/standard/cloudsql/main_test.py @@ -0,0 +1,40 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import re + +import pytest +import webtest + + +@pytest.fixture +def main(monkeypatch): + monkeypatch.setenv('CLOUDSQL_USER', 'root') + monkeypatch.setenv('CLOUDSQL_PASSWORD', '') + import main + return main + + +@pytest.mark.skipif( + not os.path.exists('/var/run/mysqld/mysqld.sock'), + reason='Local MySQL server not available.') +def test_app(main): + app = webtest.TestApp(main.app) + response = app.get('/') + + assert response.status_int == 200 + assert re.search( + re.compile(r'.*version.*', re.DOTALL), + response.body) diff --git a/appengine/standard/conftest.py b/appengine/standard/conftest.py new file mode 100644 index 00000000000..f600c548ee9 --- /dev/null +++ b/appengine/standard/conftest.py @@ -0,0 +1,40 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +# Import py.test hooks and fixtures for App Engine +from gcp_devrel.testing.appengine import ( + login, + pytest_configure, + pytest_runtest_call, + run_tasks, + testbed) +import six + +(login) +(pytest_configure) +(pytest_runtest_call) +(run_tasks) +(testbed) + + +def pytest_ignore_collect(path, config): + """Skip App Engine tests in python 3 or if no SDK is available.""" + if 'appengine/standard' in str(path): + if six.PY3: + return True + if 'GAE_SDK_PATH' not in os.environ: + return True + return False diff --git a/appengine/standard/django/README.md b/appengine/standard/django/README.md new file mode 100644 index 00000000000..ee13c8337b6 --- /dev/null +++ b/appengine/standard/django/README.md @@ -0,0 +1,10 @@ +# Getting started with Django on Google Cloud Platform on App Engine Standard + +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/1.9/intro/tutorial01/) as the +example app to deploy. + + +# Tutorial +See our [Running Django in the App Engine Standard Environment](https://cloud.google.com/python/django/appengine) tutorial for instructions for setting up and deploying this sample application. diff --git a/appengine/standard/django/app.yaml b/appengine/standard/django/app.yaml new file mode 100644 index 00000000000..15781218755 --- /dev/null +++ b/appengine/standard/django/app.yaml @@ -0,0 +1,31 @@ +# [START django_app] +runtime: python27 +api_version: 1 +threadsafe: yes + +handlers: +- url: /static + static_dir: static/ +- url: .* + script: mysite.wsgi.application + +# Only pure Python libraries can be vendored +# Python libraries that use C extensions can +# only be included if they are part of the App Engine SDK +# Using Third Party Libraries: https://cloud.google.com/appengine/docs/python/tools/using-libraries-python-27 +libraries: +- name: MySQLdb + version: 1.2.5 +# [END django_app] + +# Google App Engine limits application deployments to 10,000 uploaded files per +# version. The skip_files section allows us to skip virtual environment files +# to meet this requirement. The first 5 are the default regular expressions to +# skip, while the last one is for all env/ files. +skip_files: +- ^(.*/)?#.*#$ +- ^(.*/)?.*~$ +- ^(.*/)?.*\.py[co]$ +- ^(.*/)?.*/RCS/.*$ +- ^(.*/)?\..*$ +- ^env/.*$ diff --git a/appengine/standard/django/appengine_config.py b/appengine/standard/django/appengine_config.py new file mode 100644 index 00000000000..f18f4328eb2 --- /dev/null +++ b/appengine/standard/django/appengine_config.py @@ -0,0 +1,19 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START vendor] +from google.appengine.ext import vendor + +vendor.add('lib') +# [END vendor] diff --git a/appengine/standard/django/manage.py b/appengine/standard/django/manage.py new file mode 100755 index 00000000000..834b0091d73 --- /dev/null +++ b/appengine/standard/django/manage.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +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/standard/django/mysite/__init__.py b/appengine/standard/django/mysite/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/appengine/standard/django/mysite/settings.py b/appengine/standard/django/mysite/settings.py new file mode 100644 index 00000000000..bd8b71a445f --- /dev/null +++ b/appengine/standard/django/mysite/settings.py @@ -0,0 +1,145 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Django settings for mysite project. + +Generated by 'django-admin startproject' using Django 1.8.5. + +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 = '-c&qt=71oi^e5s8(ene*$b89^#%*0xeve$x_trs91veok9#0h0' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +# 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. +# See https://docs.djangoproject.com/en/1.10/ref/settings/ +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_CLASSES = ( + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'django.middleware.security.SecurityMiddleware', +) + +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 +# https://docs.djangoproject.com/en/1.8/ref/settings/#databases + +# [START db_setup] +if os.getenv('SERVER_SOFTWARE', '').startswith('Google App Engine'): + # Running on production App Engine, so connect to Google Cloud SQL using + # the unix socket at /cloudsql/ + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'HOST': '/cloudsql/', + 'NAME': 'polls', + 'USER': '', + 'PASSWORD': '', + } + } +else: + # Running locally so connect to either a local MySQL instance or connect to + # Cloud SQL via the proxy. To start the proxy via command line: + # + # $ cloud_sql_proxy -instances=[INSTANCE_CONNECTION_NAME]=tcp:3306 + # + # See https://cloud.google.com/sql/docs/mysql-connect-proxy + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'HOST': '127.0.0.1', + 'PORT': '3306', + 'NAME': 'polls', + 'USER': '', + 'PASSWORD': '', + } + } +# [END db_setup] + +# 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_ROOT = 'static' +STATIC_URL = '/static/' diff --git a/appengine/standard/django/mysite/urls.py b/appengine/standard/django/mysite/urls.py new file mode 100644 index 00000000000..1b387da6467 --- /dev/null +++ b/appengine/standard/django/mysite/urls.py @@ -0,0 +1,23 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from django.conf.urls import include, url +from django.contrib import admin + +from polls.views import index + +urlpatterns = [ + url(r'^$', index), + url(r'^admin/', include(admin.site.urls)), +] diff --git a/appengine/standard/django/mysite/wsgi.py b/appengine/standard/django/mysite/wsgi.py new file mode 100644 index 00000000000..ddc0aaa08b8 --- /dev/null +++ b/appengine/standard/django/mysite/wsgi.py @@ -0,0 +1,29 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +WSGI config for mysite 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", "mysite.settings") + +application = get_wsgi_application() diff --git a/appengine/standard/django/polls/__init__.py b/appengine/standard/django/polls/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/appengine/standard/django/polls/admin.py b/appengine/standard/django/polls/admin.py new file mode 100644 index 00000000000..0c5b3a09a34 --- /dev/null +++ b/appengine/standard/django/polls/admin.py @@ -0,0 +1,19 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from django.contrib import admin + +from .models import Question + +admin.site.register(Question) diff --git a/appengine/standard/django/polls/models.py b/appengine/standard/django/polls/models.py new file mode 100644 index 00000000000..9e69c2a93ff --- /dev/null +++ b/appengine/standard/django/polls/models.py @@ -0,0 +1,26 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +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) + choice_text = models.CharField(max_length=200) + votes = models.IntegerField(default=0) diff --git a/appengine/standard/django/polls/tests.py b/appengine/standard/django/polls/tests.py new file mode 100644 index 00000000000..c3b029bad97 --- /dev/null +++ b/appengine/standard/django/polls/tests.py @@ -0,0 +1,20 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Uncomment these imports and add tests here + +# from django import http +# from django.test import TestCase + +# from . import views diff --git a/appengine/standard/django/polls/views.py b/appengine/standard/django/polls/views.py new file mode 100644 index 00000000000..595ccf18ed8 --- /dev/null +++ b/appengine/standard/django/polls/views.py @@ -0,0 +1,19 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from django.http import HttpResponse + + +def index(request): + return HttpResponse("Hello, world. You're at the polls index.") diff --git a/appengine/standard/django/requirements-vendor.txt b/appengine/standard/django/requirements-vendor.txt new file mode 100644 index 00000000000..24608628760 --- /dev/null +++ b/appengine/standard/django/requirements-vendor.txt @@ -0,0 +1 @@ +Django==1.11.2 diff --git a/appengine/standard/django/requirements.txt b/appengine/standard/django/requirements.txt new file mode 100644 index 00000000000..5ee51b06219 --- /dev/null +++ b/appengine/standard/django/requirements.txt @@ -0,0 +1,2 @@ +MySQL-python==1.2.5 +Django==1.11.2 diff --git a/appengine/standard/endpoints-frameworks-v2/echo/README.md b/appengine/standard/endpoints-frameworks-v2/echo/README.md new file mode 100644 index 00000000000..c02fb9b8481 --- /dev/null +++ b/appengine/standard/endpoints-frameworks-v2/echo/README.md @@ -0,0 +1,33 @@ +## Endpoints Frameworks v2 Python Sample + +This demonstrates how to use Google Cloud Endpoints Frameworks v2 on Google App Engine Standard Environment using Python. + +## Setup + +Create a `lib` directory in which to install the Endpoints Frameworks v2 library. For more info, see [Installing a library](https://cloud.google.com/appengine/docs/python/tools/using-libraries-python-27#installing_a_library). + +Install the Endpoints Frameworks v2 library: + + $ pip install -t lib -r requirements.txt + +## Deploying to Google App Engine + +Generate a swagger file by running: `python lib/endpoints/endpointscfg.py get_swagger_spec main.EchoApi --hostname echo-api.endpoints.[YOUR-PROJECT-ID].cloud.goog` + +Remember to replace [YOUR-PROJECT-ID] with your project ID. + +To set up OAuth2, replace `your-oauth-client-id.com` under `audiences` in the annotation for `get_user_email` with your OAuth2 client ID. If you want to use Google OAuth2 Playground, use `407408718192.apps.googleusercontent.com` as your audience. To generate a JWT, go to the following address: `https://developers.google.com/oauthplayground`. + +Deploy the generated swagger spec to Google Cloud Service Management: `gcloud alpha service-management deploy echo-v1_swagger.json` + +The command returns several lines of information, including a line similar to the following: + + Service Configuration [2016-08-01r0] uploaded for service "echo-api.endpoints.[YOUR-PROJECT-ID].cloud.goog" + +Open the `app.yaml` file and in the `env_variables` section, replace [YOUR-PROJECT-ID] in `echo-api.endpoints.[YOUR-PROJECT-ID].cloud.goog` with your project ID. This is your Endpoints service name. Then replace `2016-08-01r0` with your uploaded service management configuration. + +Then, deploy the sample using `gcloud`: + + $ gcloud app deploy + +Once deployed, you can access the application at https://your-service.appspot.com diff --git a/appengine/standard/endpoints-frameworks-v2/echo/app.yaml b/appengine/standard/endpoints-frameworks-v2/echo/app.yaml new file mode 100644 index 00000000000..e8585189941 --- /dev/null +++ b/appengine/standard/endpoints-frameworks-v2/echo/app.yaml @@ -0,0 +1,32 @@ +runtime: python27 +threadsafe: true +api_version: 1 +basic_scaling: + max_instances: 2 + +#[START_EXCLUDE] +skip_files: +- ^(.*/)?#.*#$ +- ^(.*/)?.*~$ +- ^(.*/)?.*\.py[co]$ +- ^(.*/)?.*/RCS/.*$ +- ^(.*/)?\..*$ +- ^(.*/)?setuptools/script \(dev\).tmpl$ +#[END_EXCLUDE] + +handlers: +# The endpoints handler must be mapped to /_ah/api. +- url: /_ah/api/.* + script: main.api + +libraries: +- name: pycrypto + version: 2.6 +- name: ssl + version: 2.7.11 + +env_variables: + # The following values are to be replaced by information from the output of + # 'gcloud service-management deploy swagger.json' command. + ENDPOINTS_SERVICE_NAME: echo-api.endpoints.[YOUR-PROJECT-ID].cloud.goog + ENDPOINTS_SERVICE_VERSION: 2016-08-01r0 diff --git a/appengine/standard/endpoints-frameworks-v2/echo/appengine_config.py b/appengine/standard/endpoints-frameworks-v2/echo/appengine_config.py new file mode 100644 index 00000000000..3bb4ea6e3f1 --- /dev/null +++ b/appengine/standard/endpoints-frameworks-v2/echo/appengine_config.py @@ -0,0 +1,4 @@ +from google.appengine.ext import vendor + +# Add any libraries installed in the `lib` folder. +vendor.add('lib') diff --git a/appengine/standard/endpoints-frameworks-v2/echo/main.py b/appengine/standard/endpoints-frameworks-v2/echo/main.py new file mode 100644 index 00000000000..f4f06c7e063 --- /dev/null +++ b/appengine/standard/endpoints-frameworks-v2/echo/main.py @@ -0,0 +1,104 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""This is a sample Hello World API implemented using Google Cloud +Endpoints.""" + +# [START imports] +import endpoints +from protorpc import message_types +from protorpc import messages +from protorpc import remote +# [END imports] + + +# [START messages] +class EchoRequest(messages.Message): + content = messages.StringField(1) + + +class EchoResponse(messages.Message): + """A proto Message that contains a simple string field.""" + content = messages.StringField(1) + + +ECHO_RESOURCE = endpoints.ResourceContainer( + EchoRequest, + n=messages.IntegerField(2, default=1)) +# [END messages] + + +# [START echo_api] +@endpoints.api(name='echo', version='v1') +class EchoApi(remote.Service): + + @endpoints.method( + # This method takes a ResourceContainer defined above. + ECHO_RESOURCE, + # This method returns an Echo message. + EchoResponse, + path='echo', + http_method='POST', + name='echo') + def echo(self, request): + output_content = ' '.join([request.content] * request.n) + return EchoResponse(content=output_content) + + @endpoints.method( + # This method takes a ResourceContainer defined above. + ECHO_RESOURCE, + # This method returns an Echo message. + EchoResponse, + path='echo/{n}', + http_method='POST', + name='echo_path_parameter') + def echo_path_parameter(self, request): + output_content = ' '.join([request.content] * request.n) + return EchoResponse(content=output_content) + + @endpoints.method( + # This method takes a ResourceContainer defined above. + message_types.VoidMessage, + # This method returns an Echo message. + EchoResponse, + path='echo/getApiKey', + http_method='GET', + name='echo_api_key') + def echo_api_key(self, request): + return EchoResponse(content=request.get_unrecognized_field_info('key')) + + @endpoints.method( + # This method takes an empty request body. + message_types.VoidMessage, + # This method returns an Echo message. + EchoResponse, + path='echo/getUserEmail', + http_method='GET', + # Require auth tokens to have the following scopes to access this API. + scopes=[endpoints.EMAIL_SCOPE], + # OAuth2 audiences allowed in incoming tokens. + audiences=['your-oauth-client-id.com']) + def get_user_email(self, request): + user = endpoints.get_current_user() + # If there's no user defined, the request was unauthenticated, so we + # raise 401 Unauthorized. + if not user: + raise endpoints.UnauthorizedException + return EchoResponse(content=user.email()) +# [END echo_api] + + +# [START api_server] +api = endpoints.api_server([EchoApi]) +# [END api_server] diff --git a/appengine/standard/endpoints-frameworks-v2/echo/main_test.py b/appengine/standard/endpoints-frameworks-v2/echo/main_test.py new file mode 100644 index 00000000000..ed117cdf264 --- /dev/null +++ b/appengine/standard/endpoints-frameworks-v2/echo/main_test.py @@ -0,0 +1,41 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import endpoints +import mock +from protorpc import message_types +import pytest + +import main + + +def test_echo(): + api = main.EchoApi() + request = main.EchoApi.echo.remote.request_type(content='Hello world!') + response = api.echo(request) + assert 'Hello world!' == response.content + + +def test_get_user_email(): + api = main.EchoApi() + + with mock.patch('main.endpoints.get_current_user') as user_mock: + user_mock.return_value = None + with pytest.raises(endpoints.UnauthorizedException): + api.get_user_email(message_types.VoidMessage()) + + user_mock.return_value = mock.Mock() + user_mock.return_value.email.return_value = 'user@example.com' + response = api.get_user_email(message_types.VoidMessage()) + assert 'user@example.com' == response.content diff --git a/appengine/standard/endpoints-frameworks-v2/echo/requirements.txt b/appengine/standard/endpoints-frameworks-v2/echo/requirements.txt new file mode 100644 index 00000000000..7dc9ada3964 --- /dev/null +++ b/appengine/standard/endpoints-frameworks-v2/echo/requirements.txt @@ -0,0 +1,2 @@ +google-endpoints==2.0.6 +google-endpoints-api-management==1.1.2 diff --git a/appengine/standard/endpoints-frameworks-v2/quickstart/app.yaml b/appengine/standard/endpoints-frameworks-v2/quickstart/app.yaml new file mode 100644 index 00000000000..5877a48b75a --- /dev/null +++ b/appengine/standard/endpoints-frameworks-v2/quickstart/app.yaml @@ -0,0 +1,28 @@ +runtime: python27 +threadsafe: true +api_version: 1 + +skip_files: +- ^(.*/)?#.*#$ +- ^(.*/)?.*~$ +- ^(.*/)?.*\.py[co]$ +- ^(.*/)?.*/RCS/.*$ +- ^(.*/)?\..*$ +- ^lib/setuptools/script \(dev\).tmpl$ + +handlers: +# The endpoints handler must be mapped to /_ah/api. +- url: /_ah/api/.* + script: main.api + +libraries: +- name: pycrypto + version: 2.6 +- name: ssl + version: 2.7.11 + +env_variables: + # The following values are to be replaced by information from the output of + # 'gcloud service-management deploy swagger.json' command. + ENDPOINTS_SERVICE_NAME: greeting-api.endpoints.[YOUR-PROJECT-ID].cloud.goog + ENDPOINTS_SERVICE_VERSION: 2016-08-01r0 diff --git a/appengine/standard/endpoints-frameworks-v2/quickstart/appengine_config.py b/appengine/standard/endpoints-frameworks-v2/quickstart/appengine_config.py new file mode 100644 index 00000000000..3bb4ea6e3f1 --- /dev/null +++ b/appengine/standard/endpoints-frameworks-v2/quickstart/appengine_config.py @@ -0,0 +1,4 @@ +from google.appengine.ext import vendor + +# Add any libraries installed in the `lib` folder. +vendor.add('lib') diff --git a/appengine/standard/endpoints-frameworks-v2/quickstart/main.py b/appengine/standard/endpoints-frameworks-v2/quickstart/main.py new file mode 100644 index 00000000000..fc4049b1309 --- /dev/null +++ b/appengine/standard/endpoints-frameworks-v2/quickstart/main.py @@ -0,0 +1,149 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""This is a sample Hello World API implemented using Google Cloud +Endpoints.""" + +# [START imports] +import endpoints +from protorpc import message_types +from protorpc import messages +from protorpc import remote +# [END imports] + + +# [START messages] +class Greeting(messages.Message): + """Greeting that stores a message.""" + message = messages.StringField(1) + + +class GreetingCollection(messages.Message): + """Collection of Greetings.""" + items = messages.MessageField(Greeting, 1, repeated=True) + + +STORED_GREETINGS = GreetingCollection(items=[ + Greeting(message='hello world!'), + Greeting(message='goodbye world!'), +]) +# [END messages] + + +# [START greeting_api] +@endpoints.api(name='greeting', version='v1') +class GreetingApi(remote.Service): + + @endpoints.method( + # This method does not take a request message. + message_types.VoidMessage, + # This method returns a GreetingCollection message. + GreetingCollection, + path='greetings', + http_method='GET', + name='greetings.list') + def list_greetings(self, unused_request): + return STORED_GREETINGS + + # ResourceContainers are used to encapsuate a request body and url + # parameters. This one is used to represent the Greeting ID for the + # greeting_get method. + GET_RESOURCE = endpoints.ResourceContainer( + # The request body should be empty. + message_types.VoidMessage, + # Accept one url parameter: and integer named 'id' + id=messages.IntegerField(1, variant=messages.Variant.INT32)) + + @endpoints.method( + # Use the ResourceContainer defined above to accept an empty body + # but an ID in the query string. + GET_RESOURCE, + # This method returns a Greeting message. + Greeting, + # The path defines the source of the URL parameter 'id'. If not + # specified here, it would need to be in the query string. + path='greetings/{id}', + http_method='GET', + name='greetings.get') + def get_greeting(self, request): + try: + # request.id is used to access the URL parameter. + return STORED_GREETINGS.items[request.id] + except (IndexError, TypeError): + raise endpoints.NotFoundException( + 'Greeting {} not found'.format(request.id)) + # [END greeting_api] + + # [START multiply] + # This ResourceContainer is similar to the one used for get_greeting, but + # this one also contains a request body in the form of a Greeting message. + MULTIPLY_RESOURCE = endpoints.ResourceContainer( + Greeting, + times=messages.IntegerField(2, variant=messages.Variant.INT32, + required=True)) + + @endpoints.method( + # This method accepts a request body containing a Greeting message + # and a URL parameter specifying how many times to multiply the + # message. + MULTIPLY_RESOURCE, + # This method returns a Greeting message. + Greeting, + path='greetings/multiply/{times}', + http_method='POST', + name='greetings.multiply') + def multiply_greeting(self, request): + return Greeting(message=request.message * request.times) + # [END multiply] + + +# [START auth_config] +WEB_CLIENT_ID = 'replace this with your web client application ID' +ANDROID_CLIENT_ID = 'replace this with your Android client ID' +IOS_CLIENT_ID = 'replace this with your iOS client ID' +ANDROID_AUDIENCE = WEB_CLIENT_ID +ALLOWED_CLIENT_IDS = [ + WEB_CLIENT_ID, ANDROID_CLIENT_ID, IOS_CLIENT_ID, + endpoints.API_EXPLORER_CLIENT_ID] +# [END auth_config] + + +# [START authed_greeting_api] +@endpoints.api( + name='authed_greeting', + version='v1', + # Only allowed configured Client IDs to access this API. + allowed_client_ids=ALLOWED_CLIENT_IDS, + # Only allow auth tokens with the given audience to access this API. + audiences=[ANDROID_AUDIENCE], + # Require auth tokens to have the following scopes to access this API. + scopes=[endpoints.EMAIL_SCOPE]) +class AuthedGreetingApi(remote.Service): + + @endpoints.method( + message_types.VoidMessage, + Greeting, + path='greet', + http_method='POST', + name='greet') + def greet(self, request): + user = endpoints.get_current_user() + user_name = user.email() if user else 'Anonymous' + return Greeting(message='Hello, {}'.format(user_name)) +# [END authed_greeting_api] + + +# [START api_server] +api = endpoints.api_server([GreetingApi, AuthedGreetingApi]) +# [END api_server] diff --git a/appengine/standard/endpoints-frameworks-v2/quickstart/main_test.py b/appengine/standard/endpoints-frameworks-v2/quickstart/main_test.py new file mode 100644 index 00000000000..c8255a90d30 --- /dev/null +++ b/appengine/standard/endpoints-frameworks-v2/quickstart/main_test.py @@ -0,0 +1,54 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import mock +from protorpc import message_types + +import main + + +def test_list_greetings(testbed): + api = main.GreetingApi() + response = api.list_greetings(message_types.VoidMessage()) + assert len(response.items) == 2 + + +def test_get_greeting(testbed): + api = main.GreetingApi() + request = main.GreetingApi.get_greeting.remote.request_type(id=1) + response = api.get_greeting(request) + assert response.message == 'goodbye world!' + + +def test_multiply_greeting(testbed): + api = main.GreetingApi() + request = main.GreetingApi.multiply_greeting.remote.request_type( + times=4, + message='help I\'m trapped in a test case.') + response = api.multiply_greeting(request) + assert response.message == 'help I\'m trapped in a test case.' * 4 + + +def test_authed_greet(testbed): + api = main.AuthedGreetingApi() + + with mock.patch('main.endpoints.get_current_user') as user_mock: + user_mock.return_value = None + response = api.greet(message_types.VoidMessage()) + assert response.message == 'Hello, Anonymous' + + user_mock.return_value = mock.Mock() + user_mock.return_value.email.return_value = 'user@example.com' + response = api.greet(message_types.VoidMessage()) + assert response.message == 'Hello, user@example.com' diff --git a/appengine/standard/endpoints/backend/app.yaml b/appengine/standard/endpoints/backend/app.yaml new file mode 100644 index 00000000000..31030b73367 --- /dev/null +++ b/appengine/standard/endpoints/backend/app.yaml @@ -0,0 +1,16 @@ +runtime: python27 +threadsafe: true +api_version: 1 + +handlers: +# The endpoints handler must be mapped to /_ah/spi. +# Apps send requests to /_ah/api, but the endpoints service handles mapping +# those requests to /_ah/spi. +- url: /_ah/spi/.* + script: main.api + +libraries: +- name: pycrypto + version: 2.6 +- name: endpoints + version: 1.0 diff --git a/appengine/standard/endpoints/backend/main.py b/appengine/standard/endpoints/backend/main.py new file mode 100644 index 00000000000..ced7b778b3a --- /dev/null +++ b/appengine/standard/endpoints/backend/main.py @@ -0,0 +1,149 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""This is a sample Hello World API implemented using Google Cloud +Endpoints.""" + +# [START imports] +import endpoints +from protorpc import message_types +from protorpc import messages +from protorpc import remote +# [END imports] + + +# [START messages] +class Greeting(messages.Message): + """Greeting that stores a message.""" + message = messages.StringField(1) + + +class GreetingCollection(messages.Message): + """Collection of Greetings.""" + items = messages.MessageField(Greeting, 1, repeated=True) + + +STORED_GREETINGS = GreetingCollection(items=[ + Greeting(message='hello world!'), + Greeting(message='goodbye world!'), +]) +# [END messages] + + +# [START greeting_api] +@endpoints.api(name='greeting', version='v1') +class GreetingApi(remote.Service): + + @endpoints.method( + # This method does not take a request message. + message_types.VoidMessage, + # This method returns a GreetingCollection message. + GreetingCollection, + path='greetings', + http_method='GET', + name='greetings.list') + def list_greetings(self, unused_request): + return STORED_GREETINGS + + # ResourceContainers are used to encapsuate a request body and url + # parameters. This one is used to represent the Greeting ID for the + # greeting_get method. + GET_RESOURCE = endpoints.ResourceContainer( + # The request body should be empty. + message_types.VoidMessage, + # Accept one url parameter: an integer named 'id' + id=messages.IntegerField(1, variant=messages.Variant.INT32)) + + @endpoints.method( + # Use the ResourceContainer defined above to accept an empty body + # but an ID in the query string. + GET_RESOURCE, + # This method returns a Greeting message. + Greeting, + # The path defines the source of the URL parameter 'id'. If not + # specified here, it would need to be in the query string. + path='greetings/{id}', + http_method='GET', + name='greetings.get') + def get_greeting(self, request): + try: + # request.id is used to access the URL parameter. + return STORED_GREETINGS.items[request.id] + except (IndexError, TypeError): + raise endpoints.NotFoundException( + 'Greeting {} not found'.format(request.id)) + # [END greeting_api] + + # [START multiply] + # This ResourceContainer is similar to the one used for get_greeting, but + # this one also contains a request body in the form of a Greeting message. + MULTIPLY_RESOURCE = endpoints.ResourceContainer( + Greeting, + times=messages.IntegerField(2, variant=messages.Variant.INT32, + required=True)) + + @endpoints.method( + # This method accepts a request body containing a Greeting message + # and a URL parameter specifying how many times to multiply the + # message. + MULTIPLY_RESOURCE, + # This method returns a Greeting message. + Greeting, + path='greetings/multiply/{times}', + http_method='POST', + name='greetings.multiply') + def multiply_greeting(self, request): + return Greeting(message=request.message * request.times) + # [END multiply] + + +# [START auth_config] +WEB_CLIENT_ID = 'replace this with your web client application ID' +ANDROID_CLIENT_ID = 'replace this with your Android client ID' +IOS_CLIENT_ID = 'replace this with your iOS client ID' +ANDROID_AUDIENCE = WEB_CLIENT_ID +ALLOWED_CLIENT_IDS = [ + WEB_CLIENT_ID, ANDROID_CLIENT_ID, IOS_CLIENT_ID, + endpoints.API_EXPLORER_CLIENT_ID] +# [END auth_config] + + +# [START authed_greeting_api] +@endpoints.api( + name='authed_greeting', + version='v1', + # Only allowed configured Client IDs to access this API. + allowed_client_ids=ALLOWED_CLIENT_IDS, + # Only allow auth tokens with the given audience to access this API. + audiences=[ANDROID_AUDIENCE], + # Require auth tokens to have the following scopes to access this API. + scopes=[endpoints.EMAIL_SCOPE]) +class AuthedGreetingApi(remote.Service): + + @endpoints.method( + message_types.VoidMessage, + Greeting, + path='greet', + http_method='POST', + name='greet') + def greet(self, request): + user = endpoints.get_current_user() + user_name = user.email() if user else 'Anonymous' + return Greeting(message='Hello, {}'.format(user_name)) +# [END authed_greeting_api] + + +# [START api_server] +api = endpoints.api_server([GreetingApi, AuthedGreetingApi]) +# [END api_server] diff --git a/appengine/standard/endpoints/backend/main_test.py b/appengine/standard/endpoints/backend/main_test.py new file mode 100644 index 00000000000..c8255a90d30 --- /dev/null +++ b/appengine/standard/endpoints/backend/main_test.py @@ -0,0 +1,54 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import mock +from protorpc import message_types + +import main + + +def test_list_greetings(testbed): + api = main.GreetingApi() + response = api.list_greetings(message_types.VoidMessage()) + assert len(response.items) == 2 + + +def test_get_greeting(testbed): + api = main.GreetingApi() + request = main.GreetingApi.get_greeting.remote.request_type(id=1) + response = api.get_greeting(request) + assert response.message == 'goodbye world!' + + +def test_multiply_greeting(testbed): + api = main.GreetingApi() + request = main.GreetingApi.multiply_greeting.remote.request_type( + times=4, + message='help I\'m trapped in a test case.') + response = api.multiply_greeting(request) + assert response.message == 'help I\'m trapped in a test case.' * 4 + + +def test_authed_greet(testbed): + api = main.AuthedGreetingApi() + + with mock.patch('main.endpoints.get_current_user') as user_mock: + user_mock.return_value = None + response = api.greet(message_types.VoidMessage()) + assert response.message == 'Hello, Anonymous' + + user_mock.return_value = mock.Mock() + user_mock.return_value.email.return_value = 'user@example.com' + response = api.greet(message_types.VoidMessage()) + assert response.message == 'Hello, user@example.com' diff --git a/appengine/standard/endpoints/multiapi/app.yaml b/appengine/standard/endpoints/multiapi/app.yaml new file mode 100644 index 00000000000..31030b73367 --- /dev/null +++ b/appengine/standard/endpoints/multiapi/app.yaml @@ -0,0 +1,16 @@ +runtime: python27 +threadsafe: true +api_version: 1 + +handlers: +# The endpoints handler must be mapped to /_ah/spi. +# Apps send requests to /_ah/api, but the endpoints service handles mapping +# those requests to /_ah/spi. +- url: /_ah/spi/.* + script: main.api + +libraries: +- name: pycrypto + version: 2.6 +- name: endpoints + version: 1.0 diff --git a/appengine/standard/endpoints/multiapi/main.py b/appengine/standard/endpoints/multiapi/main.py new file mode 100644 index 00000000000..c25d9baff36 --- /dev/null +++ b/appengine/standard/endpoints/multiapi/main.py @@ -0,0 +1,55 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""This is a sample multi-class API implemented using Cloud Ednpoints""" + +import endpoints +from protorpc import messages +from protorpc import remote + + +class Request(messages.Message): + message = messages.StringField(1) + + +class Response(messages.Message): + message = messages.StringField(1) + + +# [START multiclass] +api_collection = endpoints.api(name='library', version='v1.0') + + +@api_collection.api_class(resource_name='shelves') +class Shelves(remote.Service): + + @endpoints.method(Request, Response, path='list') + def list(self, request): + return Response() + + +# [START books] +@api_collection.api_class(resource_name='books', path='books') +class Books(remote.Service): + + @endpoints.method(Request, Response, path='bookmark') + def bookmark(self, request): + return Response() +# [END books] +# [END multiclass] + + +# [START api_server] +api = endpoints.api_server([api_collection]) +# [END api_server] diff --git a/appengine/standard/endpoints/multiapi/main_test.py b/appengine/standard/endpoints/multiapi/main_test.py new file mode 100644 index 00000000000..a61548595ec --- /dev/null +++ b/appengine/standard/endpoints/multiapi/main_test.py @@ -0,0 +1,27 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import main + + +def test_shelves(testbed): + api = main.Shelves() + response = api.list(main.Request()) + assert response + + +def test_books(testbed): + api = main.Books() + response = api.bookmark(main.Request()) + assert response diff --git a/appengine/standard/firebase/firenotes/README.md b/appengine/standard/firebase/firenotes/README.md new file mode 100644 index 00000000000..2a544a8bbcc --- /dev/null +++ b/appengine/standard/firebase/firenotes/README.md @@ -0,0 +1,59 @@ +# Firenotes: Firebase Authentication on Google App Engine + +A simple note-taking application that stores users' notes in their own personal +notebooks separated by a unique user ID generated by Firebase. Uses Firebase +Authentication, Google App Engine, and Google Cloud Datastore. + +This sample is used on the following documentation page: + +[https://cloud.google.com/appengine/docs/python/authenticating-users-firebase-appengine/](https://cloud.google.com/appengine/docs/python/authenticating-users-firebase-appengine/) + +You'll need to have [Python 2.7](https://www.python.org/) and the [Google Cloud SDK](https://cloud.google.com/sdk/?hl=en) +installed and initialized to an App Engine project before running the code in +this sample. + +## Setup + +1. Clone this repo: + + git clone https://github.com/GoogleCloudPlatform/python-docs-samples.git + +1. Navigate to the directory that contains the sample code: + + cd python-docs-samples/appengine/standard/firebase/firenotes + +1. Within a virtualenv, install the dependencies to the backend service: + + pip install -r requirements.txt -t lib + +1. [Add Firebase to your app.](https://firebase.google.com/docs/web/setup#add_firebase_to_your_app) +1. Add your Firebase project ID to the backend’s `app.yaml` file as an +environment variable. +1. Select which providers you want to enable. Delete the providers from +`main.js` that you do no want to offer. Enable the providers you chose to keep +in the Firebase console under **Auth** > **Sign-in Method** > +**Sign-in providers**. +1. In the Firebase console, under **OAuth redirect domains**, click +**Add Domain** and enter the domain of your app on App Engine: +[PROJECT_ID].appspot.com. Do not include "http://" before the domain name. + +## Run Locally +1. Add the backend host URL to `main.js`: http://localhost:8081. +1. Navigate to the root directory of the application and start the development +server with the following command: + + dev_appserver.py frontend/app.yaml backend/app.yaml + +1. Visit [http://locahost:8080/](http://locahost:8080/) in a web browser. + +## Deploy +1. Change the backend host URL in `main.js` to +https://backend-dot-[PROJECT_ID].appspot.com. +1. Deploy the application using the Cloud SDK command-line interface: + + gcloud app deploy backend/index.yaml frontend/app.yaml backend/app.yaml + + The Cloud Datastore indexes can take a while to update, so the application + might not be fully functional immediately after deployment. + +1. View the application live at https://[PROJECT_ID].appspot.com. diff --git a/appengine/standard/firebase/firenotes/backend/.gitignore b/appengine/standard/firebase/firenotes/backend/.gitignore new file mode 100644 index 00000000000..a65b41774ad --- /dev/null +++ b/appengine/standard/firebase/firenotes/backend/.gitignore @@ -0,0 +1 @@ +lib diff --git a/appengine/standard/firebase/firenotes/backend/app.yaml b/appengine/standard/firebase/firenotes/backend/app.yaml new file mode 100644 index 00000000000..0c0a9aaf1ac --- /dev/null +++ b/appengine/standard/firebase/firenotes/backend/app.yaml @@ -0,0 +1,12 @@ +runtime: python27 +api_version: 1 +threadsafe: true +service: backend + +handlers: +- url: /.* + script: main.app + +env_variables: + # Replace with your Firebase project ID. + FIREBASE_PROJECT_ID: '' diff --git a/appengine/standard/firebase/firenotes/backend/appengine_config.py b/appengine/standard/firebase/firenotes/backend/appengine_config.py new file mode 100644 index 00000000000..c903d9a0ac5 --- /dev/null +++ b/appengine/standard/firebase/firenotes/backend/appengine_config.py @@ -0,0 +1,18 @@ +# Copyright 2016 Google Inc. +# +# 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.appengine.ext import vendor + +# Add any libraries installed in the "lib" folder. +vendor.add('lib') diff --git a/appengine/standard/firebase/firenotes/backend/index.yaml b/appengine/standard/firebase/firenotes/backend/index.yaml new file mode 100644 index 00000000000..b0ebb4ffea8 --- /dev/null +++ b/appengine/standard/firebase/firenotes/backend/index.yaml @@ -0,0 +1,22 @@ +indexes: + +# AUTOGENERATED + +# This index.yaml is automatically updated whenever the dev_appserver +# detects that a new type of query is run. If you want to manage the +# index.yaml file manually, remove the above marker line (the line +# saying "# AUTOGENERATED"). If you want to manage some indexes +# manually, move them above the marker line. The index.yaml file is +# automatically uploaded to the admin console when you next deploy +# your application using appcfg.py. + +- kind: Note + ancestor: yes + properties: + - name: created + +- kind: Note + ancestor: yes + properties: + - name: created + direction: desc diff --git a/appengine/standard/firebase/firenotes/backend/main.py b/appengine/standard/firebase/firenotes/backend/main.py new file mode 100644 index 00000000000..86921e19c13 --- /dev/null +++ b/appengine/standard/firebase/firenotes/backend/main.py @@ -0,0 +1,133 @@ +# Copyright 2016 Google Inc. +# +# 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 + +from flask import Flask, jsonify, request +import flask_cors +from google.appengine.ext import ndb +import google.auth.transport.requests +import google.oauth2.id_token +import requests_toolbelt.adapters.appengine + +# Use the App Engine Requests adapter. This makes sure that Requests uses +# URLFetch. +requests_toolbelt.adapters.appengine.monkeypatch() +HTTP_REQUEST = google.auth.transport.requests.Request() + +app = Flask(__name__) +flask_cors.CORS(app) + + +# [START note] +class Note(ndb.Model): + """NDB model class for a user's note. + + Key is user id from decrypted token. + """ + friendly_id = ndb.StringProperty() + message = ndb.TextProperty() + created = ndb.DateTimeProperty(auto_now_add=True) +# [END note] + + +# [START query_database] +def query_database(user_id): + """Fetches all notes associated with user_id. + + Notes are ordered them by date created, with most recent note added + first. + """ + ancestor_key = ndb.Key(Note, user_id) + query = Note.query(ancestor=ancestor_key).order(-Note.created) + notes = query.fetch() + + note_messages = [] + + for note in notes: + note_messages.append({ + 'friendly_id': note.friendly_id, + 'message': note.message, + 'created': note.created + }) + + return note_messages +# [END query_database] + + +# [START list_notes] +@app.route('/notes', methods=['GET']) +def list_notes(): + """Returns a list of notes added by the current Firebase user.""" + + # Verify Firebase auth. + # [START verify_token] + id_token = request.headers['Authorization'].split(' ').pop() + claims = google.oauth2.id_token.verify_firebase_token( + id_token, HTTP_REQUEST) + if not claims: + return 'Unauthorized', 401 + # [END verify_token] + + notes = query_database(claims['sub']) + + return jsonify(notes) +# [END list_notes] + + +# [START add_note] +@app.route('/notes', methods=['POST', 'PUT']) +def add_note(): + """ + Adds a note to the user's notebook. The request should be in this format: + + { + "message": "note message." + } + """ + + # Verify Firebase auth. + id_token = request.headers['Authorization'].split(' ').pop() + claims = google.oauth2.id_token.verify_firebase_token( + id_token, HTTP_REQUEST) + if not claims: + return 'Unauthorized', 401 + + # [START create_entity] + data = request.get_json() + + # Populates note properties according to the model, + # with the user ID as the key name. + note = Note( + parent=ndb.Key(Note, claims['sub']), + message=data['message']) + + # Some providers do not provide one of these so either can be used. + note.friendly_id = claims.get('name', claims.get('email', 'Unknown')) + # [END create_entity] + + # Stores note in database. + note.put() + + return 'OK', 200 +# [END add_note] + + +@app.errorhandler(500) +def server_error(e): + # Log the error and stacktrace. + logging.exception('An error occurred during a request.') + return 'An internal error occurred.', 500 +# [END app] diff --git a/appengine/standard/firebase/firenotes/backend/main_test.py b/appengine/standard/firebase/firenotes/backend/main_test.py new file mode 100644 index 00000000000..3aa7e944734 --- /dev/null +++ b/appengine/standard/firebase/firenotes/backend/main_test.py @@ -0,0 +1,96 @@ +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json + +from google.appengine.ext import ndb +import jwt +import mock +import pytest + + +@pytest.fixture +def app(): + # Remove any existing pyjwt handlers, as firebase_helper will register + # its own. + try: + jwt.unregister_algorithm('RS256') + except KeyError: + pass + + import main + main.app.testing = True + return main.app.test_client() + + +@pytest.fixture +def mock_token(): + patch = mock.patch('google.oauth2.id_token.verify_firebase_token') + with patch as mock_verify: + yield mock_verify + + +@pytest.fixture +def test_data(): + from main import Note + ancestor_key = ndb.Key(Note, '123') + notes = [ + Note(parent=ancestor_key, message='1'), + Note(parent=ancestor_key, message='2') + ] + ndb.put_multi(notes) + yield + + +def test_list_notes_with_mock_token(testbed, app, mock_token, test_data): + mock_token.return_value = {'sub': '123'} + + r = app.get('/notes', headers={'Authorization': 'Bearer 123'}) + assert r.status_code == 200 + + data = json.loads(r.data) + assert len(data) == 2 + assert data[0]['message'] == '2' + + +def test_list_notes_with_bad_mock_token(testbed, app, mock_token): + mock_token.return_value = None + + r = app.get('/notes', headers={'Authorization': 'Bearer 123'}) + assert r.status_code == 401 + + +def test_add_note_with_mock_token(testbed, app, mock_token): + mock_token.return_value = {'sub': '123'} + + r = app.post( + '/notes', + data=json.dumps({'message': 'Hello, world!'}), + content_type='application/json', + headers={'Authorization': 'Bearer 123'}) + + assert r.status_code == 200 + + from main import Note + + results = Note.query().fetch() + assert len(results) == 1 + assert results[0].message == 'Hello, world!' + + +def test_add_note_with_bad_mock_token(testbed, app, mock_token): + mock_token.return_value = None + + r = app.post('/notes', headers={'Authorization': 'Bearer 123'}) + assert r.status_code == 401 diff --git a/appengine/standard/firebase/firenotes/backend/requirements.txt b/appengine/standard/firebase/firenotes/backend/requirements.txt new file mode 100644 index 00000000000..e63566a21fa --- /dev/null +++ b/appengine/standard/firebase/firenotes/backend/requirements.txt @@ -0,0 +1,6 @@ +Flask==0.12.2 +pyjwt==1.5.2 +flask-cors==3.0.2 +google-auth==1.0.1 +requests==2.18.1 +requests-toolbelt==0.8.0 diff --git a/appengine/standard/firebase/firenotes/frontend/app.yaml b/appengine/standard/firebase/firenotes/frontend/app.yaml new file mode 100644 index 00000000000..e205ff25c09 --- /dev/null +++ b/appengine/standard/firebase/firenotes/frontend/app.yaml @@ -0,0 +1,15 @@ +runtime: python27 +api_version: 1 +service: default +threadsafe: true + +handlers: + +# root +- url: / + static_files: index.html + upload: index.html + +- url: /(.+) + static_files: \1 + upload: (.+) diff --git a/appengine/standard/firebase/firenotes/frontend/index.html b/appengine/standard/firebase/firenotes/frontend/index.html new file mode 100644 index 00000000000..b874a75d337 --- /dev/null +++ b/appengine/standard/firebase/firenotes/frontend/index.html @@ -0,0 +1,44 @@ + + + + + + + + + + + + + Firenotes + + +
    +

    Firenotes

    +

    Sign in to access your notebook

    +
    +
    + +
    +

    Welcome, !

    +

    Enter a note and save it to your personal notebook

    +
    +
    +
    + +
    +
    + + +
    +
    +
    + +
    +
    + + diff --git a/appengine/standard/firebase/firenotes/frontend/main.js b/appengine/standard/firebase/firenotes/frontend/main.js new file mode 100644 index 00000000000..c5f3580be4a --- /dev/null +++ b/appengine/standard/firebase/firenotes/frontend/main.js @@ -0,0 +1,156 @@ +// Copyright 2016, Google, Inc. +// 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(){ + // This is the host for the backend. + // TODO: When running Firenotes locally, set to http://localhost:8081. Before + // deploying the application to a live production environment, change to + // https://backend-dot-.appspot.com as specified in the + // backend's app.yaml file. + var backendHostUrl = ''; + + // Initialize Firebase + // TODO: Replace with your project's customized code snippet + var config = { + apiKey: "", + authDomain: ".firebaseapp.com", + databaseURL: "https://.firebaseio.com", + storageBucket: ".appspot.com", + }; + + // This is passed into the backend to authenticate the user. + var userIdToken = null; + + // Firebase log-in + function configureFirebaseLogin() { + + firebase.initializeApp(config); + + // [START onAuthStateChanged] + firebase.auth().onAuthStateChanged(function(user) { + if (user) { + $('#logged-out').hide(); + var name = user.displayName; + + /* If the provider gives a display name, use the name for the + personal welcome message. Otherwise, use the user's email. */ + var welcomeName = name ? name : user.email; + + user.getToken().then(function(idToken) { + userIdToken = idToken; + + /* Now that the user is authenicated, fetch the notes. */ + fetchNotes(); + + $('#user').text(welcomeName); + $('#logged-in').show(); + + }); + + } else { + $('#logged-in').hide(); + $('#logged-out').show(); + + } + // [END onAuthStateChanged] + + }); + + } + + // [START configureFirebaseLoginWidget] + // Firebase log-in widget + function configureFirebaseLoginWidget() { + var uiConfig = { + 'signInSuccessUrl': '/', + 'signInOptions': [ + // Leave the lines as is for the providers you want to offer your users. + firebase.auth.GoogleAuthProvider.PROVIDER_ID, + firebase.auth.FacebookAuthProvider.PROVIDER_ID, + firebase.auth.TwitterAuthProvider.PROVIDER_ID, + firebase.auth.GithubAuthProvider.PROVIDER_ID, + firebase.auth.EmailAuthProvider.PROVIDER_ID + ], + // Terms of service url + 'tosUrl': '', + }; + + var ui = new firebaseui.auth.AuthUI(firebase.auth()); + ui.start('#firebaseui-auth-container', uiConfig); + } + // [END configureFirebaseLoginWidget] + + // [START fetchNotes] + // Fetch notes from the backend. + function fetchNotes() { + $.ajax(backendHostUrl + '/notes', { + /* Set header for the XMLHttpRequest to get data from the web server + associated with userIdToken */ + headers: { + 'Authorization': 'Bearer ' + userIdToken + } + }).then(function(data){ + $('#notes-container').empty(); + // Iterate over user data to display user's notes from database. + data.forEach(function(note){ + $('#notes-container').append($('

    ').text(note.message)); + }); + }); + } + // [END fetchNotes] + + // [START signOutBtn] + // Sign out a user + var signOutBtn =$('#sign-out'); + signOutBtn.click(function(event) { + event.preventDefault(); + + firebase.auth().signOut().then(function() { + console.log("Sign out successful"); + }, function(error) { + console.log(error); + }); + }); + // [END signOutBtn] + + // [START saveNoteBtn] + // Save a note to the backend + var saveNoteBtn = $('#add-note'); + saveNoteBtn.click(function(event) { + event.preventDefault(); + + var noteField = $('#note-content'); + var note = noteField.val(); + noteField.val(""); + + /* Send note data to backend, storing in database with existing data + associated with userIdToken */ + $.ajax(backendHostUrl + '/notes', { + headers: { + 'Authorization': 'Bearer ' + userIdToken + }, + method: 'POST', + data: JSON.stringify({'message': note}), + contentType : 'application/json' + }).then(function(){ + // Refresh notebook display. + fetchNotes(); + }); + + }); + // [END saveNoteBtn] + + configureFirebaseLogin(); + configureFirebaseLoginWidget(); + +}); diff --git a/appengine/standard/firebase/firenotes/frontend/style.css b/appengine/standard/firebase/firenotes/frontend/style.css new file mode 100644 index 00000000000..19b4f1d69bd --- /dev/null +++ b/appengine/standard/firebase/firenotes/frontend/style.css @@ -0,0 +1,44 @@ +/* + Copyright 2016, Google, Inc. + 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. +*/ + +body { + font-family: "helvetica", sans-serif; + text-align: center; +} + +form { + padding: 5px 0 10px; + margin-bottom: 30px; +} +h3,legend { + font-weight: 400; + padding: 18px 0 15px; + margin: 0 0 0; +} + +div.form-group { + margin-bottom: 10px; +} + +input, textarea { + width: 250px; + font-size: 14px; + padding: 6px; +} + +textarea { + vertical-align: top; + height: 75px; +} diff --git a/appengine/standard/firebase/firetactoe/README.md b/appengine/standard/firebase/firetactoe/README.md new file mode 100644 index 00000000000..e1ebe86ec71 --- /dev/null +++ b/appengine/standard/firebase/firetactoe/README.md @@ -0,0 +1,44 @@ +# Tic Tac Toe, using Firebase, on App Engine Standard + +This sample shows how to use the [Firebase](https://firebase.google.com/) +realtime database to implement a simple Tic Tac Toe game on [Google App Engine +Standard](https://cloud.google.com/appengine). + +## Setup + +Make sure you have the [Google Cloud SDK](https://cloud.google.com/sdk/) +installed. You'll need this to test and deploy your App Engine app. + +### Authentication + +* Create a project in the [Firebase + console](https://firebase.google.com/console) +* In the Overview section, click 'Add Firebase to your web app' and replace the + contents of the file + [`templates/_firebase_config.html`](templates/_firebase_config.html) with the + given snippet. This provides credentials for the javascript client. +* For running the sample locally, you'll need to download a service account to + provide credentials that would normally be provided automatically in the App + Engine environment. Click the gear icon in the Firebase Console and select + 'Permissions'; then go to the 'Service accounts' tab. Download a new or + existing App Engine service account credentials file. Then set the environment + variable `GOOGLE_APPLICATION_CREDENTIALS` to the path to this file: + + export GOOGLE_APPLICATION_CREDENTIALS=/path/to/credentials.json + + This allows the server to create unique secure tokens for each user for + Firebase to validate. + +### Install dependencies + +Before running or deploying this application, install the dependencies using +[pip](http://pip.readthedocs.io/en/stable/): + + pip install -t lib -r requirements.txt + +## Running the sample + + dev_appserver.py . + +For more information on running or deploying the sample, see the [App Engine +Standard README](../../README.md). diff --git a/appengine/standard/firebase/firetactoe/app.yaml b/appengine/standard/firebase/firetactoe/app.yaml new file mode 100644 index 00000000000..4e474a199d9 --- /dev/null +++ b/appengine/standard/firebase/firetactoe/app.yaml @@ -0,0 +1,11 @@ +runtime: python27 +api_version: 1 +threadsafe: true + +handlers: +- url: /static + static_dir: static + +- url: /.* + script: firetactoe.app + login: required diff --git a/appengine/standard/firebase/firetactoe/appengine_config.py b/appengine/standard/firebase/firetactoe/appengine_config.py new file mode 100644 index 00000000000..25d6ed86866 --- /dev/null +++ b/appengine/standard/firebase/firetactoe/appengine_config.py @@ -0,0 +1,10 @@ +import os.path + +from google.appengine.ext import vendor + +# Add any libraries installed in the "lib" folder. +vendor.add('lib') + +# Patch os.path.expanduser. This should be fixed in GAE +# versions released after Nov 2016. +os.path.expanduser = lambda path: path diff --git a/appengine/standard/firebase/firetactoe/firetactoe.py b/appengine/standard/firebase/firetactoe/firetactoe.py new file mode 100644 index 00000000000..ec84abf90c8 --- /dev/null +++ b/appengine/standard/firebase/firetactoe/firetactoe.py @@ -0,0 +1,274 @@ +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tic Tac Toe with the Firebase API""" + +import base64 +try: + from functools import lru_cache +except ImportError: + from functools32 import lru_cache +import json +import os +import re +import time +import urllib + + +import flask +from flask import request +from google.appengine.api import app_identity +from google.appengine.api import users +from google.appengine.ext import ndb +import httplib2 +from oauth2client.client import GoogleCredentials + + +_FIREBASE_CONFIG = '_firebase_config.html' + +_IDENTITY_ENDPOINT = ('https://identitytoolkit.googleapis.com/' + 'google.identity.identitytoolkit.v1.IdentityToolkit') +_FIREBASE_SCOPES = [ + 'https://www.googleapis.com/auth/firebase.database', + 'https://www.googleapis.com/auth/userinfo.email'] + +_X_WIN_PATTERNS = [ + 'XXX......', '...XXX...', '......XXX', 'X..X..X..', '.X..X..X.', + '..X..X..X', 'X...X...X', '..X.X.X..'] +_O_WIN_PATTERNS = map(lambda s: s.replace('X', 'O'), _X_WIN_PATTERNS) + +X_WINS = map(lambda s: re.compile(s), _X_WIN_PATTERNS) +O_WINS = map(lambda s: re.compile(s), _O_WIN_PATTERNS) + + +app = flask.Flask(__name__) + + +# Memoize the value, to avoid parsing the code snippet every time +@lru_cache() +def _get_firebase_db_url(): + """Grabs the databaseURL from the Firebase config snippet. Regex looks + scary, but all it is doing is pulling the 'databaseURL' field from the + Firebase javascript snippet""" + regex = re.compile(r'\bdatabaseURL\b.*?["\']([^"\']+)') + cwd = os.path.dirname(__file__) + try: + with open(os.path.join(cwd, 'templates', _FIREBASE_CONFIG)) as f: + url = next(regex.search(line) for line in f if regex.search(line)) + except StopIteration: + raise ValueError( + 'Error parsing databaseURL. Please copy Firebase web snippet ' + 'into templates/{}'.format(_FIREBASE_CONFIG)) + return url.group(1) + + +# Memoize the authorized http, to avoid fetching new access tokens +@lru_cache() +def _get_http(): + """Provides an authed http object.""" + http = httplib2.Http() + # Use application default credentials to make the Firebase calls + # https://firebase.google.com/docs/reference/rest/database/user-auth + creds = GoogleCredentials.get_application_default().create_scoped( + _FIREBASE_SCOPES) + creds.authorize(http) + return http + + +def _send_firebase_message(u_id, message=None): + """Updates data in firebase. If a message is provided, then it updates + the data at /channels/ with the message using the PATCH + http method. If no message is provided, then the data at this location + is deleted using the DELETE http method + """ + url = '{}/channels/{}.json'.format(_get_firebase_db_url(), u_id) + + if message: + return _get_http().request(url, 'PATCH', body=message) + else: + return _get_http().request(url, 'DELETE') + + +def create_custom_token(uid, valid_minutes=60): + """Create a secure token for the given id. + + This method is used to create secure custom JWT tokens to be passed to + clients. It takes a unique id (uid) that will be used by Firebase's + security rules to prevent unauthorized access. In this case, the uid will + be the channel id which is a combination of user_id and game_key + """ + + # use the app_identity service from google.appengine.api to get the + # project's service account email automatically + client_email = app_identity.get_service_account_name() + + now = int(time.time()) + # encode the required claims + # per https://firebase.google.com/docs/auth/server/create-custom-tokens + payload = base64.b64encode(json.dumps({ + 'iss': client_email, + 'sub': client_email, + 'aud': _IDENTITY_ENDPOINT, + 'uid': uid, # the important parameter, as it will be the channel id + 'iat': now, + 'exp': now + (valid_minutes * 60), + })) + # add standard header to identify this as a JWT + header = base64.b64encode(json.dumps({'typ': 'JWT', 'alg': 'RS256'})) + to_sign = '{}.{}'.format(header, payload) + # Sign the jwt using the built in app_identity service + return '{}.{}'.format(to_sign, base64.b64encode( + app_identity.sign_blob(to_sign)[1])) + + +class Game(ndb.Model): + """All the data we store for a game""" + userX = ndb.UserProperty() + userO = ndb.UserProperty() + board = ndb.StringProperty() + moveX = ndb.BooleanProperty() + winner = ndb.StringProperty() + winning_board = ndb.StringProperty() + + def to_json(self): + d = self.to_dict() + d['winningBoard'] = d.pop('winning_board') + return json.dumps(d, default=lambda user: user.user_id()) + + def send_update(self): + """Updates Firebase's copy of the board.""" + message = self.to_json() + # send updated game state to user X + _send_firebase_message( + self.userX.user_id() + self.key.id(), message=message) + # send updated game state to user O + if self.userO: + _send_firebase_message( + self.userO.user_id() + self.key.id(), message=message) + + def _check_win(self): + if self.moveX: + # O just moved, check for O wins + wins = O_WINS + potential_winner = self.userO.user_id() + else: + # X just moved, check for X wins + wins = X_WINS + potential_winner = self.userX.user_id() + + for win in wins: + if win.match(self.board): + self.winner = potential_winner + self.winning_board = win.pattern + return + + # In case of a draw, everyone loses. + if ' ' not in self.board: + self.winner = 'Noone' + + def make_move(self, position, user): + # If the user is a player, and it's their move + if (user in (self.userX, self.userO)) and ( + self.moveX == (user == self.userX)): + boardList = list(self.board) + # If the spot you want to move to is blank + if (boardList[position] == ' '): + boardList[position] = 'X' if self.moveX else 'O' + self.board = ''.join(boardList) + self.moveX = not self.moveX + self._check_win() + self.put() + self.send_update() + return + + +# [START move_route] +@app.route('/move', methods=['POST']) +def move(): + game = Game.get_by_id(request.args.get('g')) + position = int(request.form.get('i')) + if not (game and (0 <= position <= 8)): + return 'Game not found, or invalid position', 400 + game.make_move(position, users.get_current_user()) + return '' +# [END move_route] + + +# [START route_delete] +@app.route('/delete', methods=['POST']) +def delete(): + game = Game.get_by_id(request.args.get('g')) + if not game: + return 'Game not found', 400 + user = users.get_current_user() + _send_firebase_message(user.user_id() + game.key.id(), message=None) + return '' +# [END route_delete] + + +@app.route('/opened', methods=['POST']) +def opened(): + game = Game.get_by_id(request.args.get('g')) + if not game: + return 'Game not found', 400 + game.send_update() + return '' + + +@app.route('/') +def main_page(): + """Renders the main page. When this page is shown, we create a new + channel to push asynchronous updates to the client.""" + user = users.get_current_user() + game_key = request.args.get('g') + + if not game_key: + game_key = user.user_id() + game = Game(id=game_key, userX=user, moveX=True, board=' '*9) + game.put() + else: + game = Game.get_by_id(game_key) + if not game: + return 'No such game', 404 + if not game.userO: + game.userO = user + game.put() + + # [START pass_token] + # choose a unique identifier for channel_id + channel_id = user.user_id() + game_key + # encrypt the channel_id and send it as a custom token to the + # client + # Firebase's data security rules will be able to decrypt the + # token and prevent unauthorized access + client_auth_token = create_custom_token(channel_id) + _send_firebase_message(channel_id, message=game.to_json()) + + # game_link is a url that you can open in another browser to play + # against this player + game_link = '{}?g={}'.format(request.base_url, game_key) + + # push all the data to the html template so the client will + # have access + template_values = { + 'token': client_auth_token, + 'channel_id': channel_id, + 'me': user.user_id(), + 'game_key': game_key, + 'game_link': game_link, + 'initial_message': urllib.unquote(game.to_json()) + } + + return flask.render_template('fire_index.html', **template_values) + # [END pass_token] diff --git a/appengine/standard/firebase/firetactoe/firetactoe_test.py b/appengine/standard/firebase/firetactoe/firetactoe_test.py new file mode 100644 index 00000000000..f5ec1ca9573 --- /dev/null +++ b/appengine/standard/firebase/firetactoe/firetactoe_test.py @@ -0,0 +1,161 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import re + +from google.appengine.api import users +from google.appengine.ext import ndb +import httplib2 +import pytest +import webtest + +import firetactoe + + +class MockHttp(object): + """Mock the Http object, so we can set what the response will be.""" + def __init__(self, status, content=''): + self.content = content + self.status = status + self.request_url = None + + def __call__(self, *args, **kwargs): + return self + + def request(self, url, method, content='', *args, **kwargs): + self.request_url = url + self.request_method = method + self.request_content = content + return self, self.content + + +@pytest.fixture +def app(testbed, monkeypatch, login): + # Don't let the _get_http function memoize its value + firetactoe._get_http.cache_clear() + + # Provide a test firebase config. The following will set the databaseURL + # databaseURL: "http://firebase.com/test-db-url" + monkeypatch.setattr( + firetactoe, '_FIREBASE_CONFIG', '../firetactoe_test.py') + + login(id='38') + + firetactoe.app.debug = True + return webtest.TestApp(firetactoe.app) + + +def test_index_new_game(app, monkeypatch): + mock_http = MockHttp(200, content=json.dumps({'access_token': '123'})) + monkeypatch.setattr(httplib2, 'Http', mock_http) + + response = app.get('/') + + assert 'g=' in response.body + # Look for the unique game token + assert re.search( + r'initGame[^\n]+\'[\w+/=]+\.[\w+/=]+\.[\w+/=]+\'', response.body) + + assert firetactoe.Game.query().count() == 1 + + assert mock_http.request_url.startswith( + 'http://firebase.com/test-db-url/channels/') + assert mock_http.request_method == 'PATCH' + + +def test_index_existing_game(app, monkeypatch): + mock_http = MockHttp(200, content=json.dumps({'access_token': '123'})) + monkeypatch.setattr(httplib2, 'Http', mock_http) + userX = users.User('x@example.com', _user_id='123') + firetactoe.Game(id='razem', userX=userX).put() + + response = app.get('/?g=razem') + + assert 'g=' in response.body + # Look for the unique game token + assert re.search( + r'initGame[^\n]+\'[\w+/=]+\.[\w+/=]+\.[\w+/=]+\'', response.body) + + assert firetactoe.Game.query().count() == 1 + game = ndb.Key('Game', 'razem').get() + assert game is not None + assert game.userO.user_id() == '38' + + assert mock_http.request_url.startswith( + 'http://firebase.com/test-db-url/channels/') + assert mock_http.request_method == 'PATCH' + + +def test_index_nonexisting_game(app, monkeypatch): + mock_http = MockHttp(200, content=json.dumps({'access_token': '123'})) + monkeypatch.setattr(httplib2, 'Http', mock_http) + firetactoe.Game(id='razem', userX=users.get_current_user()).put() + + app.get('/?g=razemfrazem', status=404) + + assert mock_http.request_url is None + + +def test_opened(app, monkeypatch): + mock_http = MockHttp(200, content=json.dumps({'access_token': '123'})) + monkeypatch.setattr(httplib2, 'Http', mock_http) + firetactoe.Game(id='razem', userX=users.get_current_user()).put() + + app.post('/opened?g=razem', status=200) + + assert mock_http.request_url.startswith( + 'http://firebase.com/test-db-url/channels/') + assert mock_http.request_method == 'PATCH' + + +def test_bad_move(app, monkeypatch): + mock_http = MockHttp(200, content=json.dumps({'access_token': '123'})) + monkeypatch.setattr(httplib2, 'Http', mock_http) + firetactoe.Game( + id='razem', userX=users.get_current_user(), board=9*' ', + moveX=True).put() + + app.post('/move?g=razem', {'i': 10}, status=400) + + assert mock_http.request_url is None + + +def test_move(app, monkeypatch): + mock_http = MockHttp(200, content=json.dumps({'access_token': '123'})) + monkeypatch.setattr(httplib2, 'Http', mock_http) + firetactoe.Game( + id='razem', userX=users.get_current_user(), board=9*' ', + moveX=True).put() + + app.post('/move?g=razem', {'i': 0}, status=200) + + game = ndb.Key('Game', 'razem').get() + assert game.board == 'X' + (8 * ' ') + + assert mock_http.request_url.startswith( + 'http://firebase.com/test-db-url/channels/') + assert mock_http.request_method == 'PATCH' + + +def test_delete(app, monkeypatch): + mock_http = MockHttp(200, content=json.dumps({'access_token': '123'})) + monkeypatch.setattr(httplib2, 'Http', mock_http) + firetactoe.Game(id='razem', userX=users.get_current_user()).put() + + app.post('/delete?g=razem', status=200) + + assert mock_http.request_url.startswith( + 'http://firebase.com/test-db-url/channels/') + assert mock_http.request_method == 'DELETE' diff --git a/appengine/standard/firebase/firetactoe/requirements.txt b/appengine/standard/firebase/firetactoe/requirements.txt new file mode 100644 index 00000000000..1fb0307821e --- /dev/null +++ b/appengine/standard/firebase/firetactoe/requirements.txt @@ -0,0 +1,5 @@ +flask==0.12.2 +requests==2.18.1 +requests_toolbelt==0.8.0 +oauth2client==4.1.1 +functools32==3.2.3.post2; python_version < "3" diff --git a/appengine/standard/firebase/firetactoe/rest_api.py b/appengine/standard/firebase/firetactoe/rest_api.py new file mode 100644 index 00000000000..a067953a8f1 --- /dev/null +++ b/appengine/standard/firebase/firetactoe/rest_api.py @@ -0,0 +1,114 @@ +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Demonstration of the Firebase REST API in Python""" + +try: + from functools import lru_cache +except ImportError: + from functools32 import lru_cache +# [START rest_writing_data] +import json + +import httplib2 +from oauth2client.client import GoogleCredentials + +_FIREBASE_SCOPES = [ + 'https://www.googleapis.com/auth/firebase.database', + 'https://www.googleapis.com/auth/userinfo.email'] + + +# Memoize the authorized http, to avoid fetching new access tokens +@lru_cache() +def _get_http(): + """Provides an authed http object.""" + http = httplib2.Http() + # Use application default credentials to make the Firebase calls + # https://firebase.google.com/docs/reference/rest/database/user-auth + creds = GoogleCredentials.get_application_default().create_scoped( + _FIREBASE_SCOPES) + creds.authorize(http) + return http + + +def firebase_put(path, value=None): + """Writes data to Firebase. + + An HTTP PUT writes an entire object at the given database path. Updates to + fields cannot be performed without overwriting the entire object + + Args: + path - the url to the Firebase object to write. + value - a json string. + """ + response, content = _get_http().request(path, method='PUT', body=value) + return json.loads(content) + + +def firebase_patch(path, value=None): + """Update specific children or fields + + An HTTP PATCH allows specific children or fields to be updated without + overwriting the entire object. + + Args: + path - the url to the Firebase object to write. + value - a json string. + """ + response, content = _get_http().request(path, method='PATCH', body=value) + return json.loads(content) + + +def firebase_post(path, value=None): + """Add an object to an existing list of data. + + An HTTP POST allows an object to be added to an existing list of data. + A successful request will be indicated by a 200 OK HTTP status code. The + response content will contain a new attribute "name" which is the key for + the child added. + + Args: + path - the url to the Firebase list to append to. + value - a json string. + """ + response, content = _get_http().request(path, method='POST', body=value) + return json.loads(content) +# [END rest_writing_data] + + +def firebase_get(path): + """Read the data at the given path. + + An HTTP GET request allows reading of data at a particular path. + A successful request will be indicated by a 200 OK HTTP status code. + The response will contain the data being retrieved. + + Args: + path - the url to the Firebase object to read. + """ + response, content = _get_http().request(path, method='GET') + return json.loads(content) + + +def firebase_delete(path): + """Removes the data at a particular path. + + An HTTP DELETE removes the data at a particular path. A successful request + will be indicated by a 200 OK HTTP status code with a response containing + JSON null. + + Args: + path - the url to the Firebase object to delete. + """ + response, content = _get_http().request(path, method='DELETE') diff --git a/appengine/standard/firebase/firetactoe/static/main.css b/appengine/standard/firebase/firetactoe/static/main.css new file mode 100644 index 00000000000..f314eab5b37 --- /dev/null +++ b/appengine/standard/firebase/firetactoe/static/main.css @@ -0,0 +1,82 @@ +body { + font-family: 'Helvetica'; +} + +#board { + width:152px; + height: 152px; + margin: 20px auto; +} + +#display-area { + text-align: center; +} + +#other-player, #your-move, #their-move, #you-won, #you-lost { + display: none; +} + +#display-area.waiting #other-player { + display: block; +} + +#display-area.waiting #board, #display-area.waiting #this-game { + display: none; +} +#display-area.won #you-won { + display: block; +} +#display-area.lost #you-lost { + display: block; +} +#display-area.your-move #your-move { + display: block; +} +#display-area.their-move #their-move { + display: block; +} + + +#this-game { + font-size: 9pt; +} + +div.cell { + float: left; + width: 50px; + height: 50px; + border: none; + margin: 0px; + padding: 0px; + box-sizing: border-box; + + line-height: 50px; + font-family: "Helvetica"; + font-size: 16pt; + text-align: center; +} + +.your-move div.cell:hover { + background: lightgrey; +} + +.your-move div.cell:empty:hover { + background: lightblue; + cursor: pointer; +} + +div.l { + border-right: 1pt solid black; +} + +div.r { + border-left: 1pt solid black; +} + +div.t { + border-bottom: 1pt solid black; +} + +div.b { + border-top: 1pt solid black; +} diff --git a/appengine/standard/firebase/firetactoe/static/main.js b/appengine/standard/firebase/firetactoe/static/main.js new file mode 100644 index 00000000000..980509574f2 --- /dev/null +++ b/appengine/standard/firebase/firetactoe/static/main.js @@ -0,0 +1,178 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +/** + * @fileoverview Tic-Tac-Toe, using the Firebase API + */ + +/** + * @param gameKey - a unique key for this game. + * @param me - my user id. + * @param token - secure token passed from the server + * @param channelId - id of the 'channel' we'll be listening to + */ +function initGame(gameKey, me, token, channelId, initialMessage) { + var state = { + gameKey: gameKey, + me: me + }; + + // This is our Firebase realtime DB path that we'll listen to for updates + // We'll initialize this later in openChannel() + var channel = null; + + /** + * Updates the displayed game board. + */ + function updateGame(newState) { + $.extend(state, newState); + + $('.cell').each(function(i) { + var square = $(this); + var value = state.board[i]; + square.html(' ' === value ? '' : value); + + if (state.winner && state.winningBoard) { + if (state.winningBoard[i] === value) { + if (state.winner === state.me) { + square.css('background', 'green'); + } else { + square.css('background', 'red'); + } + } else { + square.css('background', ''); + } + } + }); + + var displayArea = $('#display-area'); + + if (!state.userO) { + displayArea[0].className = 'waiting'; + } else if (state.winner === state.me) { + displayArea[0].className = 'won'; + } else if (state.winner) { + displayArea[0].className = 'lost'; + } else if (isMyMove()) { + displayArea[0].className = 'your-move'; + } else { + displayArea[0].className = 'their-move'; + } + } + + function isMyMove() { + return !state.winner && (state.moveX === (state.userX === state.me)); + } + + function myPiece() { + return state.userX === state.me ? 'X' : 'O'; + } + + /** + * Send the user's latest move back to the server + */ + function moveInSquare(e) { + var id = $(e.currentTarget).index(); + if (isMyMove() && state.board[id] === ' ') { + $.post('/move', {i: id}); + } + } + + /** + * This method lets the server know that the user has opened the channel + * After this method is called, the server may begin to send updates + */ + function onOpened() { + $.post('/opened'); + } + + /** + * This deletes the data associated with the Firebase path + * it is critical that this data be deleted since it costs money + */ + function deleteChannel() { + $.post('/delete'); + } + + /** + * This method is called every time an event is fired from Firebase + * it updates the entire game state and checks for a winner + * if a player has won the game, this function calls the server to delete + * the data stored in Firebase + */ + function onMessage(newState) { + updateGame(newState); + + // now check to see if there is a winner + if (channel && state.winner && state.winningBoard) { + channel.off(); //stop listening on this path + deleteChannel(); //delete the data we wrote + } + } + + /** + * This function opens a realtime communication channel with Firebase + * It logs in securely using the client token passed from the server + * then it sets up a listener on the proper database path (also passed by server) + * finally, it calls onOpened() to let the server know it is ready to receive messages + */ + function openChannel() { + // [START auth_login] + // sign into Firebase with the token passed from the server + firebase.auth().signInWithCustomToken(token).catch(function(error) { + console.log('Login Failed!', error.code); + console.log('Error message: ', error.message); + }); + // [END auth_login] + + // [START add_listener] + // setup a database reference at path /channels/channelId + channel = firebase.database().ref('channels/' + channelId); + // add a listener to the path that fires any time the value of the data changes + channel.on('value', function(data) { + onMessage(data.val()); + }); + // [END add_listener] + onOpened(); + // let the server know that the channel is open + } + + /** + * This function opens a communication channel with the server + * then it adds listeners to all the squares on the board + * next it pulls down the initial game state from template values + * finally it updates the game state with those values by calling onMessage() + */ + function initialize() { + // Always include the gamekey in our requests + $.ajaxPrefilter(function(opts) { + if (opts.url.indexOf('?') > 0) + opts.url += '&g=' + state.gameKey; + else + opts.url += '?g=' + state.gameKey; + }); + + $('#board').on('click', '.cell', moveInSquare); + + openChannel(); + + onMessage(initialMessage); + } + + setTimeout(initialize, 100); +} diff --git a/appengine/standard/firebase/firetactoe/templates/_firebase_config.html b/appengine/standard/firebase/firetactoe/templates/_firebase_config.html new file mode 100644 index 00000000000..25898c985af --- /dev/null +++ b/appengine/standard/firebase/firetactoe/templates/_firebase_config.html @@ -0,0 +1,3 @@ +REPLACE ME WITH YOUR FIREBASE WEBAPP CODE SNIPPET: + +https://console.firebase.google.com/project/_/overview diff --git a/appengine/standard/firebase/firetactoe/templates/fire_index.html b/appengine/standard/firebase/firetactoe/templates/fire_index.html new file mode 100644 index 00000000000..3b18380b533 --- /dev/null +++ b/appengine/standard/firebase/firetactoe/templates/fire_index.html @@ -0,0 +1,43 @@ + + + + + {% include "_firebase_config.html" %} + + + + + + +

    +

    Firebase-enabled Tic Tac Toe

    +
    + Waiting for another player to join.
    + Send them this link to play:
    + +
    +
    Your move! Click a square to place your piece.
    +
    Waiting for other player to move...
    +
    You won this game!
    +
    You lost this game.
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + Quick link to this game: {{ game_link }} +
    +
    + + diff --git a/appengine/standard/flask/hello_world/.gitignore b/appengine/standard/flask/hello_world/.gitignore new file mode 100644 index 00000000000..a65b41774ad --- /dev/null +++ b/appengine/standard/flask/hello_world/.gitignore @@ -0,0 +1 @@ +lib diff --git a/appengine/standard/flask/hello_world/README.md b/appengine/standard/flask/hello_world/README.md new file mode 100644 index 00000000000..04ec9402ab1 --- /dev/null +++ b/appengine/standard/flask/hello_world/README.md @@ -0,0 +1,11 @@ +# App Engine Standard Flask Hello World + +This sample shows how to use [Flask](http://flask.pocoo.org/) with Google App +Engine Standard. + +Before running or deploying this application, install the dependencies using +[pip](http://pip.readthedocs.io/en/stable/): + + pip install -t lib -r requirements.txt + +For more information, see the [App Engine Standard README](../../README.md) diff --git a/appengine/standard/flask/hello_world/app.yaml b/appengine/standard/flask/hello_world/app.yaml new file mode 100644 index 00000000000..f041d384c05 --- /dev/null +++ b/appengine/standard/flask/hello_world/app.yaml @@ -0,0 +1,7 @@ +runtime: python27 +api_version: 1 +threadsafe: true + +handlers: +- url: /.* + script: main.app diff --git a/appengine/standard/flask/hello_world/appengine_config.py b/appengine/standard/flask/hello_world/appengine_config.py new file mode 100644 index 00000000000..c903d9a0ac5 --- /dev/null +++ b/appengine/standard/flask/hello_world/appengine_config.py @@ -0,0 +1,18 @@ +# Copyright 2016 Google Inc. +# +# 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.appengine.ext import vendor + +# Add any libraries installed in the "lib" folder. +vendor.add('lib') diff --git a/appengine/standard/flask/hello_world/main.py b/appengine/standard/flask/hello_world/main.py new file mode 100644 index 00000000000..6e5a9d82fd1 --- /dev/null +++ b/appengine/standard/flask/hello_world/main.py @@ -0,0 +1,34 @@ +# Copyright 2016 Google Inc. +# +# 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 + +from flask import Flask + + +app = Flask(__name__) + + +@app.route('/') +def hello(): + return 'Hello World!' + + +@app.errorhandler(500) +def server_error(e): + # Log the error and stacktrace. + logging.exception('An error occurred during a request.') + return 'An internal error occurred.', 500 +# [END app] diff --git a/appengine/standard/flask/hello_world/main_test.py b/appengine/standard/flask/hello_world/main_test.py new file mode 100644 index 00000000000..ab6db4fe5df --- /dev/null +++ b/appengine/standard/flask/hello_world/main_test.py @@ -0,0 +1,27 @@ +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import 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 diff --git a/appengine/standard/flask/hello_world/requirements.txt b/appengine/standard/flask/hello_world/requirements.txt new file mode 100644 index 00000000000..52ab1a39019 --- /dev/null +++ b/appengine/standard/flask/hello_world/requirements.txt @@ -0,0 +1 @@ +Flask==0.12.2 diff --git a/appengine/standard/flask/tutorial/.gitignore b/appengine/standard/flask/tutorial/.gitignore new file mode 100644 index 00000000000..a65b41774ad --- /dev/null +++ b/appengine/standard/flask/tutorial/.gitignore @@ -0,0 +1 @@ +lib diff --git a/appengine/standard/flask/tutorial/README.md b/appengine/standard/flask/tutorial/README.md new file mode 100644 index 00000000000..46f841935ec --- /dev/null +++ b/appengine/standard/flask/tutorial/README.md @@ -0,0 +1,11 @@ +# App Engine Standard Flask Tutorial App + +This sample shows how to use [Flask](http://flask.pocoo.org/) to handle +requests, forms, templates, and static files on Google App Engine Standard. + +Before running or deploying this application, install the dependencies using +[pip](http://pip.readthedocs.io/en/stable/): + + pip install -t lib -r requirements.txt + +For more information, see the [App Engine Standard README](../../README.md) diff --git a/appengine/standard/flask/tutorial/app.yaml b/appengine/standard/flask/tutorial/app.yaml new file mode 100644 index 00000000000..862e62b5273 --- /dev/null +++ b/appengine/standard/flask/tutorial/app.yaml @@ -0,0 +1,11 @@ +runtime: python27 +api_version: 1 +threadsafe: true + +# [START handlers] +handlers: +- url: /static + static_dir: static +- url: /.* + script: main.app +# [END handlers] diff --git a/appengine/standard/flask/tutorial/appengine_config.py b/appengine/standard/flask/tutorial/appengine_config.py new file mode 100644 index 00000000000..c03915197f7 --- /dev/null +++ b/appengine/standard/flask/tutorial/appengine_config.py @@ -0,0 +1,20 @@ +# Copyright 2016 Google Inc. +# +# 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 vendor] +from google.appengine.ext import vendor + +# Add any libraries installed in the "lib" folder. +vendor.add('lib') +# [END vendor] diff --git a/appengine/standard/flask/tutorial/main.py b/appengine/standard/flask/tutorial/main.py new file mode 100644 index 00000000000..feed0fbcf62 --- /dev/null +++ b/appengine/standard/flask/tutorial/main.py @@ -0,0 +1,58 @@ +# Copyright 2016 Google Inc. +# +# 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 + +# [START imports] +from flask import Flask, render_template, request +# [END imports] + +# [START create_app] +app = Flask(__name__) +# [END create_app] + + +# [START form] +@app.route('/form') +def form(): + return render_template('form.html') +# [END form] + + +# [START submitted] +@app.route('/submitted', methods=['POST']) +def submitted_form(): + name = request.form['name'] + email = request.form['email'] + site = request.form['site_url'] + comments = request.form['comments'] + + # [END submitted] + # [START render_template] + return render_template( + 'submitted_form.html', + name=name, + email=email, + site=site, + comments=comments) + # [END render_template] + + +@app.errorhandler(500) +def server_error(e): + # Log the error and stacktrace. + logging.exception('An error occurred during a request.') + return 'An internal error occurred.', 500 +# [END app] diff --git a/appengine/standard/flask/tutorial/main_test.py b/appengine/standard/flask/tutorial/main_test.py new file mode 100644 index 00000000000..9dbd16c9458 --- /dev/null +++ b/appengine/standard/flask/tutorial/main_test.py @@ -0,0 +1,38 @@ +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + + +@pytest.fixture +def app(): + import main + main.app.testing = True + return main.app.test_client() + + +def test_form(app): + r = app.get('/form') + assert r.status_code == 200 + assert 'Submit a form' in r.data.decode('utf-8') + + +def test_submitted_form(app): + r = app.post('/submitted', data={ + 'name': 'Inigo Montoya', + 'email': 'inigo@example.com', + 'site_url': 'http://example.com', + 'comments': ''}) + assert r.status_code == 200 + assert 'Inigo Montoya' in r.data.decode('utf-8') diff --git a/appengine/standard/flask/tutorial/requirements.txt b/appengine/standard/flask/tutorial/requirements.txt new file mode 100644 index 00000000000..52ab1a39019 --- /dev/null +++ b/appengine/standard/flask/tutorial/requirements.txt @@ -0,0 +1 @@ +Flask==0.12.2 diff --git a/appengine/standard/flask/tutorial/static/style.css b/appengine/standard/flask/tutorial/static/style.css new file mode 100644 index 00000000000..378932a78f9 --- /dev/null +++ b/appengine/standard/flask/tutorial/static/style.css @@ -0,0 +1,3 @@ +.pagetitle { + color: #800080; +} diff --git a/appengine/standard/flask/tutorial/templates/form.html b/appengine/standard/flask/tutorial/templates/form.html new file mode 100644 index 00000000000..159752799e8 --- /dev/null +++ b/appengine/standard/flask/tutorial/templates/form.html @@ -0,0 +1,26 @@ + + + Submit a form + + + +
    +
    +

    Submit a form

    +
    +
    +
    + +
    + +
    + +
    + +
    + +
    +
    +
    + + diff --git a/appengine/standard/flask/tutorial/templates/submitted_form.html b/appengine/standard/flask/tutorial/templates/submitted_form.html new file mode 100644 index 00000000000..15f530088f6 --- /dev/null +++ b/appengine/standard/flask/tutorial/templates/submitted_form.html @@ -0,0 +1,23 @@ + + + Submitted form + + + +
    +
    +

    Form submitted

    +
    +
    +

    Thanks for your submission, {{name}}!

    +

    Here's a review of the information that you sent:

    +

    + Name: {{name}}
    + Email: {{email}}
    + Website URL: {{site}}
    + Comments: {{comments}} +

    +
    +
    + + diff --git a/appengine/standard/hello_world/app.yaml b/appengine/standard/hello_world/app.yaml new file mode 100644 index 00000000000..f041d384c05 --- /dev/null +++ b/appengine/standard/hello_world/app.yaml @@ -0,0 +1,7 @@ +runtime: python27 +api_version: 1 +threadsafe: true + +handlers: +- url: /.* + script: main.app diff --git a/appengine/standard/hello_world/main.py b/appengine/standard/hello_world/main.py new file mode 100644 index 00000000000..c26ad7a80f6 --- /dev/null +++ b/appengine/standard/hello_world/main.py @@ -0,0 +1,26 @@ +# Copyright 2016 Google Inc. +# +# 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 webapp2 + + +class MainPage(webapp2.RequestHandler): + def get(self): + self.response.headers['Content-Type'] = 'text/plain' + self.response.write('Hello, World!') + + +app = webapp2.WSGIApplication([ + ('/', MainPage), +], debug=True) diff --git a/appengine/standard/hello_world/main_test.py b/appengine/standard/hello_world/main_test.py new file mode 100644 index 00000000000..64670c5981d --- /dev/null +++ b/appengine/standard/hello_world/main_test.py @@ -0,0 +1,26 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import webtest + +import main + + +def test_get(): + app = webtest.TestApp(main.app) + + response = app.get('/') + + assert response.status_int == 200 + assert response.body == 'Hello, World!' diff --git a/appengine/standard/i18n/README.md b/appengine/standard/i18n/README.md new file mode 100644 index 00000000000..ab982f2b81e --- /dev/null +++ b/appengine/standard/i18n/README.md @@ -0,0 +1,128 @@ +# App Engine Internationalization Sample + +A simple example app showing how to build an internationalized app +with App Engine. + +## What to internationalize + +There are lots of things to internationalize with your web +applications. + +1. Strings in Python code +2. Strings in HTML template +3. Strings in Javascript +4. Common strings + - Country Names, Language Names, etc. +5. Formatting + - Date/Time formatting + - Number formatting + - Currency +6. Timezone conversion + +This example only covers first 3 basic scenarios above. In order to +cover other aspects, I recommend using +[Babel](http://babel.edgewall.org/) and [pytz] +(http://pypi.python.org/pypi/gaepytz). Also, you may want to use +[webapp2_extras.i18n](http://webapp-improved.appspot.com/tutorials/i18n.html) +module. + +## Wait, so why not webapp2_extras.i18n? + +webapp2_extras.i18n doesn't cover how to internationalize strings in +Javascript code. Additionally it depends on babel and pytz, which +means you need to deploy babel and pytz alongside with your code. I'd +like to show a reasonably minimum example for string +internationalization in Python code, jinja2 templates, as well as +Javascript. + +## How to run this example + +First of all, please install babel in your local Python environment. + +### Wait, you just said I don't need babel, are you crazy? + +As I said before, you don't need to deploy babel with this +application, but you need to locally use pybabel script which is +provided by babel distribution in order to extract the strings, manage +and compile the translations file. + +### Extract strings in Python code and Jinja2 templates to translate + +Move into this project directory and invoke the following command: + + $ env PYTHONPATH=/google_appengine_sdk/lib/jinja2 \ + pybabel extract -o locales/messages.pot -F main.mapping . + +This command creates a `locales/messages.pot` file in the `locales` +directory which contains all the string found in your Python code and +Jija2 tempaltes. + +Since the babel configration file `main.mapping` contains a reference +to `jinja2.ext.babel_extract` helper function which is provided by +jinja2 distribution bundled with the App Engine SDK, you need to add a +PYTHONPATH environment variable pointing to the jinja2 directory in +the SDK. + +### Manage and compile translations. + +Create an initial translation source by the following command: + + $ pybabel init -l ja -d locales -i locales/messages.pot + +Open `locales/ja/LC_MESSAGES/messages.po` with any text editor and +translate the strings, then compile the file by the following command: + + $ pybabel compile -d locales + +If any of the strings changes, you can extract the strings again, and +update the translations by the following command: + + $ pybabel update -l ja -d locales -i locales/messages.pot + +Note: If you run `pybabel init` against an existant translations file, +you will lose your translations. + + +### Extract strings in Javascript code and compile translations + + $ pybabel extract -o locales/jsmessages.pot -F js.mapping . + $ pybabel init -l ja -d locales -i locales/jsmessages.pot -D jsmessages + +Open `locales/ja/LC_MESSAGES/jsmessages.po` and translate it. + + $ pybabel compile -d locales -D jsmessages + +### Running locally & deploying + +Refer to the [App Engine Samples README](../README.md) for information on how to run and deploy this sample. + +## How it works + +As you can see it in the `appengine_config.py` file, our +`main.application` is wrapped by the `i18n_utils.I18nMiddleware` WSGI +middleware. When a request comes in, this middleware parses the +`HTTP_ACCEPT_LANGUAGE` HTTP header, loads available translation +files(`messages.mo`) from the application directory, and install the +`gettext` and `ngettext` functions to the `__builtin__` namespace in +the Python runtime. + +For strings in Jinja2 templates, there is the `i18n_utils.BaseHandler` +class from which you can extend in order to have a handy property +named `jinja2_env` that lazily initializes Jinja2 environment for you +with the `jinja2.ext.i18n` extention, and similar to the +`I18nMiddleware`, installs `gettext` and `ngettext` functions to the +global namespace of the Jinja2 environment. + +## What about Javascript? + +The `BaseHandler` class also installs the `get_i18n_js_tag()` instance +method to the Jinja2 global namespace. When you use this function in +your Jinja2 template (like in the `index.jinja2` file), you will get a +set of Javascript functions; `gettext`, `ngettext`, and `format` on +the string type. The `format` function can be used with `ngettext`ed +strings for number formatting. See this example: + + window.alert(ngettext( + 'You need to provide at least {0} item.', + 'You need to provide at least {0} items.', + n).format(n); diff --git a/appengine/standard/i18n/app.yaml b/appengine/standard/i18n/app.yaml new file mode 100644 index 00000000000..c37918a6de7 --- /dev/null +++ b/appengine/standard/i18n/app.yaml @@ -0,0 +1,24 @@ +application: i18n-sample-python +runtime: python27 +api_version: 1 +version: 1 +threadsafe: true + + +handlers: +- url: /favicon\.ico + static_files: favicon.ico + upload: favicon\.ico + +- url: /static + static_dir: static +- url: /.* + script: main.application + +libraries: +- name: webapp2 + version: latest +- name: jinja2 + version: latest +- name: webob + version: 1.2.3 diff --git a/appengine/standard/i18n/appengine_config.py b/appengine/standard/i18n/appengine_config.py new file mode 100644 index 00000000000..1076922e162 --- /dev/null +++ b/appengine/standard/i18n/appengine_config.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python +# +# Copyright 2013 Google Inc. +# +# 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 configuration file for applying the I18nMiddleware.""" + + +from i18n_utils import I18nMiddleware + + +def webapp_add_wsgi_middleware(app): + """Applying the I18nMiddleware to our HelloWorld app. + + Args: + app: The WSGI application object that you want to wrap with the + I18nMiddleware. + + Returns: + The wrapped WSGI application. + """ + + app = I18nMiddleware(app) + return app diff --git a/appengine/standard/i18n/i18n_utils.py b/appengine/standard/i18n/i18n_utils.py new file mode 100644 index 00000000000..12eb5e59179 --- /dev/null +++ b/appengine/standard/i18n/i18n_utils.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python +# +# Copyright 2013 Google Inc. +# +# 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. + +"""A small module for i18n of webapp2 and jinja2 based apps. + +The idea of this example, especially for how to translate strings in +Javascript is originally from an implementation of Django i18n. +""" + + +import gettext +import json +import os + +import jinja2 + +import webapp2 + +from webob import Request + + +def _get_plural_forms(js_translations): + """Extracts the parameters for what constitutes a plural. + + Args: + js_translations: GNUTranslations object to be converted. + + Returns: + A tuple of: + A formula for what constitutes a plural + How many plural forms there are + """ + plural = None + n_plural = 2 + if '' in js_translations._catalog: + for l in js_translations._catalog[''].split('\n'): + if l.startswith('Plural-Forms:'): + plural = l.split(':', 1)[1].strip() + print "plural is %s" % plural + if plural is not None: + for raw_element in plural.split(';'): + element = raw_element.strip() + if element.startswith('nplurals='): + n_plural = int(element.split('=', 1)[1]) + elif element.startswith('plural='): + plural = element.split('=', 1)[1] + print "plural is now %s" % plural + else: + n_plural = 2 + plural = '(n == 1) ? 0 : 1' + return plural, n_plural + + +def convert_translations_to_dict(js_translations): + """Convert a GNUTranslations object into a dict for jsonifying. + + Args: + js_translations: GNUTranslations object to be converted. + + Returns: + A dictionary representing the GNUTranslations object. + """ + plural, n_plural = _get_plural_forms(js_translations) + + translations_dict = {'plural': plural, 'catalog': {}, 'fallback': None} + if js_translations._fallback is not None: + translations_dict['fallback'] = convert_translations_to_dict( + js_translations._fallback + ) + for key, value in js_translations._catalog.items(): + if key == '': + continue + if type(key) in (str, unicode): + translations_dict['catalog'][key] = value + elif type(key) == tuple: + if key[0] not in translations_dict['catalog']: + translations_dict['catalog'][key[0]] = [''] * n_plural + translations_dict['catalog'][key[0]][int(key[1])] = value + return translations_dict + + +class BaseHandler(webapp2.RequestHandler): + """A base handler for installing i18n-aware Jinja2 environment.""" + + @webapp2.cached_property + def jinja2_env(self): + """Cached property for a Jinja2 environment. + + Returns: + Jinja2 Environment object. + """ + + jinja2_env = jinja2.Environment( + loader=jinja2.FileSystemLoader( + os.path.join(os.path.dirname(__file__), 'templates')), + extensions=['jinja2.ext.i18n']) + jinja2_env.install_gettext_translations( + self.request.environ['i18n_utils.active_translation']) + jinja2_env.globals['get_i18n_js_tag'] = self.get_i18n_js_tag + return jinja2_env + + def get_i18n_js_tag(self): + """Generates a Javascript tag for i18n in Javascript. + + This instance method is installed to the global namespace of + the Jinja2 environment, so you can invoke this method just + like `{{ get_i18n_js_tag() }}` from anywhere in your Jinja2 + template. + + Returns: + A 'javascript' HTML tag which contains functions and + translation messages for i18n. + """ + + template = self.jinja2_env.get_template('javascript_tag.jinja2') + return template.render({'javascript_body': self.get_i18n_js()}) + + def get_i18n_js(self): + """Generates a Javascript body for i18n in Javascript. + + If you want to load these javascript code from a static HTML + file, you need to create another handler which just returns + the code generated by this function. + + Returns: + Actual javascript code for functions and translation + messages for i18n. + """ + + try: + js_translations = gettext.translation( + 'jsmessages', 'locales', fallback=False, + languages=self.request.environ[ + 'i18n_utils.preferred_languages'], + codeset='utf-8') + except IOError: + template = self.jinja2_env.get_template('null_i18n_js.jinja2') + return template.render() + + translations_dict = convert_translations_to_dict(js_translations) + template = self.jinja2_env.get_template('i18n_js.jinja2') + return template.render( + {'translations': json.dumps(translations_dict, indent=1)}) + + +class I18nMiddleware(object): + """A WSGI middleware for i18n. + + This middleware determines users' preferred language, loads the + translations files, and install it to the builtin namespace of the + Python runtime. + """ + + def __init__(self, app, default_language='en', locale_path=None): + """A constructor for this middleware. + + Args: + app: A WSGI app that you want to wrap with this + middleware. + default_language: fallback language; ex: 'en', 'ja', etc. + locale_path: A directory containing the translations + file. (defaults to 'locales' directory) + """ + + self.app = app + if locale_path is None: + locale_path = os.path.join( + os.path.abspath(os.path.dirname(__file__)), 'locales') + self.locale_path = locale_path + self.default_language = default_language + + def __call__(self, environ, start_response): + """Called by WSGI when a request comes in. + + Args: + environ: A dict holding environment variables. + start_response: A WSGI callable (PEP333). + + Returns: + Application response data as an iterable. It just returns + the return value of the inner WSGI app. + """ + req = Request(environ) + preferred_languages = list(req.accept_language) + if self.default_language not in preferred_languages: + preferred_languages.append(self.default_language) + translation = gettext.translation( + 'messages', self.locale_path, fallback=True, + languages=preferred_languages, codeset='utf-8') + translation.install(unicode=True, names=['gettext', 'ngettext']) + environ['i18n_utils.active_translation'] = translation + environ['i18n_utils.preferred_languages'] = preferred_languages + + return self.app(environ, start_response) diff --git a/appengine/standard/i18n/js.mapping b/appengine/standard/i18n/js.mapping new file mode 100644 index 00000000000..e5feb205882 --- /dev/null +++ b/appengine/standard/i18n/js.mapping @@ -0,0 +1 @@ +[javascript: **.js] diff --git a/appengine/standard/i18n/locales/en/LC_MESSAGES/jsmessages.mo b/appengine/standard/i18n/locales/en/LC_MESSAGES/jsmessages.mo new file mode 100644 index 0000000000000000000000000000000000000000..be32af390463ac6fc988cfcbcb1427fab2312d7d GIT binary patch literal 526 zcmaJ;+e!m55M9Nmee~If@lC<3yA`FzZt+$x&|;BA!MEvl8dI~$l4M0cL_g1e@DI#Z zK|yigz&YVeW^(ejy7Cf*S`JtX*lNX(>IL4O=K-%vQFQmE-vf^>NGSui#;PgItf^p* zkLXG(Yrh_it(jBl+2xeokaf~%4o7tj11+Q`kXcuQ!>U#kUsG+YXCo)%lpQz1vBGfh z6?)g0Lt%_il$555$5#bAv4lR{#!k@_hfbR9ury;?23d#irY!BIX)v;DdX(+dzkQ*n z^$pQ^R#?Q{-f(!RWMZu7$79u4RP4;y z%5l*3Lb|+j0G~A9(6F9Ewi%BuM}zNdvLua9jP|6%M$dJ4nx`kfT`QFO0Lpu`j(pzu t8S8%g+RbNV*+7?ODz)Iyok$;_lO`MD328oaIK~MnNR$0!FaH0}J^-9Xoels1 literal 0 HcmV?d00001 diff --git a/appengine/standard/i18n/locales/en/LC_MESSAGES/jsmessages.po b/appengine/standard/i18n/locales/en/LC_MESSAGES/jsmessages.po new file mode 100644 index 00000000000..c2458e006eb --- /dev/null +++ b/appengine/standard/i18n/locales/en/LC_MESSAGES/jsmessages.po @@ -0,0 +1,18 @@ +msgid "" +msgstr "" +"Project-Id-Version: App Engine i18n sample\n" +"Report-Msgid-Bugs-To: tmatsuo@google.com\n" +"POT-Creation-Date: 2013-01-11 12:50-0800\n" +"PO-Revision-Date: 2013-01-11 12:53-0800\n" +"Last-Translator: Takashi Matsuo \n" +"Language-Team: en \n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.6\n" + +#: static/js/main.js:5 +msgid "Hello World from javascript!" +msgstr "" + diff --git a/appengine/standard/i18n/locales/en/LC_MESSAGES/messages.mo b/appengine/standard/i18n/locales/en/LC_MESSAGES/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..8f82b9c84d5817d285270f0d16e0cee70b035682 GIT binary patch literal 590 zcmb7BO;5r=5XFyE4<0;gGTihq*ad^QiV>kIrj@2tJ+Cm-nzBo_i;l>?Ib)S9Zl8VB> zow3CjCe}>hD%X#y4vwa?LOH7bWyA~jU^I-(m|tUS9%DI2l|8-4t$v zWY`~EdkM{qbr_XdKE|_oR$^)-1W~6So_2b@Bp438c#vY(l5+e5_PDWxx-S~&3lsuG zUG6v0Z}~nGF`=h?+36RrfuaSXDI3f?U|y(7hpAP%GV@$ZN)CCth64tt#-`*l8(2&py+#T|a`au)U+9+6J-fMg1uFRzcK! zuWPg;&2ubfypW@3x5ujH`Upl3%9iBXb2q`(H*Y!bge(S{<;`^_p>>}-?}9X07KP\n" +"Language-Team: en \n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.6\n" + +#: main.py:10 +msgid "Hello World from Python code!" +msgstr "" + +#: templates/index.jinja2:6 +msgid "Hello World from Jinja2 template!" +msgstr "" + diff --git a/appengine/standard/i18n/locales/ja/LC_MESSAGES/jsmessages.mo b/appengine/standard/i18n/locales/ja/LC_MESSAGES/jsmessages.mo new file mode 100644 index 0000000000000000000000000000000000000000..3464c12b9c06ec39217927384bb82c8c360f9cea GIT binary patch literal 538 zcmZuuziSjh6kf$@OKXc~?`&ox$~n0UG4T>3cX))wK)T7^yUE1anPp}+B%Pa16f8pQ ztu1UV{99~R5Nz@<2@eR5dgqo6iTCjl;nSTY*`s%f z@P~Nv5{+@N>%5u6!n+C%<&g}f*L8fD%)C3qGP3PCdxJh`X9Ya3YZzOlErQMm7J{s5 zgXs>|&PO&4O3&GPqXH|OfLO^G8n>pLQwForRq5%_w+rcB7-Qcp_k3?xO*oJ-*>5=N~VAw8\n" +"Language-Team: ja \n" +"Plural-Forms: nplurals=1; plural=0\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.6\n" + +#: static/js/main.js:5 +msgid "Hello World from javascript!" +msgstr "Javascript から、こんにちは、世界!" + diff --git a/appengine/standard/i18n/locales/ja/LC_MESSAGES/messages.mo b/appengine/standard/i18n/locales/ja/LC_MESSAGES/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..683a0359ca3ccd7be6db2af2f0d64852110609fd GIT binary patch literal 650 zcma)3J#Q2-5M4g1Evbn#`2k*bFCiS8Toe+H5Oga}rxn!B-JH+IuD!Coi0jx(6#=4% zLQjnZ1r0@h3yqN|`3uYjq##0prKg#(-+VoPy>b0CpmU9Ild#?ff1CmNtAvcOLePY_ zSAyUT*_OL%q4t%YT|PQjJj*;?-itwn*+ zSeN1OanAM(a!1wJeHtQ#PMoZqMaPRaJNWgsWxFegzx?cv&iv8wf93rI4(Hya literal 0 HcmV?d00001 diff --git a/appengine/standard/i18n/locales/ja/LC_MESSAGES/messages.po b/appengine/standard/i18n/locales/ja/LC_MESSAGES/messages.po new file mode 100644 index 00000000000..9b0713c01d0 --- /dev/null +++ b/appengine/standard/i18n/locales/ja/LC_MESSAGES/messages.po @@ -0,0 +1,23 @@ + +msgid "" +msgstr "" +"Project-Id-Version: App Engine i18n sample\n" +"Report-Msgid-Bugs-To: tmatsuo@google.com\n" +"POT-Creation-Date: 2013-01-11 12:46-0800\n" +"PO-Revision-Date: 2013-01-11 12:53-0800\n" +"Last-Translator: Takashi Matsuo \n" +"Language-Team: ja \n" +"Plural-Forms: nplurals=1; plural=0\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.6\n" + +#: main.py:10 +msgid "Hello World from Python code!" +msgstr "Python のコードから、こんにちは、世界!" + +#: templates/index.jinja2:6 +msgid "Hello World from Jinja2 template!" +msgstr "Jinja2 template から、こんにちは、世界!" + diff --git a/appengine/standard/i18n/locales/jsmessages.pot b/appengine/standard/i18n/locales/jsmessages.pot new file mode 100644 index 00000000000..113e91b08f6 --- /dev/null +++ b/appengine/standard/i18n/locales/jsmessages.pot @@ -0,0 +1,18 @@ +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2013-01-11 12:50-0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.6\n" + +#: static/js/main.js:5 +msgid "Hello World from javascript!" +msgstr "" + diff --git a/appengine/standard/i18n/locales/messages.pot b/appengine/standard/i18n/locales/messages.pot new file mode 100644 index 00000000000..10048d75de6 --- /dev/null +++ b/appengine/standard/i18n/locales/messages.pot @@ -0,0 +1,22 @@ +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2013-01-11 12:46-0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.6\n" + +#: main.py:10 +msgid "Hello World from Python code!" +msgstr "" + +#: templates/index.jinja2:6 +msgid "Hello World from Jinja2 template!" +msgstr "" + diff --git a/appengine/standard/i18n/locales/pl/LC_MESSAGES/jsmessages.mo b/appengine/standard/i18n/locales/pl/LC_MESSAGES/jsmessages.mo new file mode 100644 index 0000000000000000000000000000000000000000..c4aa6b078c8dc72e495cef4929a040a17582c73c GIT binary patch literal 584 zcmaJ;!EO{W3|$bXIdbM;4y{^2hGbjNlFn>P+XX7^id8iNiJLp?ou*`xD9Kb*>TmF0 z{Q|y)WI-xY5tg20KR?H@{qyOQAERWC6P_i!%)wt)B|r0Z!ri0M=OMK1VAT3h&H zy}rVDB^)=j5!9Y!N!O zV#h-lSYsu`9%C5nQD<$ZscdauEI&2u#1qE!8au^E62@HYF)o-8Aja}FXMDnW60&o; zR=MlHb^E_{a}1F+9!<~~t(VZ?Hw>2w7MtEsJ^aM9n4{?i?1(jly#%MBn$M54Dy{FP z#ZnI*HG6M;A0!yJ2}8Yikc2r?@0hw4a0nbydQ0$WJ%;V=ja$|G+1<%BuLOtN+YO&J zd^gpIR5RW!7H5mu{Z>WEixX=inKa9dOG}CLJ-%`pl{tX+5`Cap4{^;V_qsgunmn7C j)~=N4Bup+;EIyDSpB9_$a3kRuFGvF~-\n" +"Language-Team: pl \n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && " +"(n%100<10 || n%100>=20) ? 1 : 2)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.6\n" + +#: static/js/main.js:5 +msgid "Hello World from javascript!" +msgstr "" + diff --git a/appengine/standard/i18n/main.mapping b/appengine/standard/i18n/main.mapping new file mode 100644 index 00000000000..190f5a275eb --- /dev/null +++ b/appengine/standard/i18n/main.mapping @@ -0,0 +1,8 @@ +# Extraction from Python source files +[python: **.py] +# Extraction from jinja2 templates +[jinja2: templates/**.jinja2] +encoding = utf-8 +[extractors] +jinja2 = jinja2.ext:babel_extract + diff --git a/appengine/standard/i18n/main.py b/appengine/standard/i18n/main.py new file mode 100644 index 00000000000..e2277049510 --- /dev/null +++ b/appengine/standard/i18n/main.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python +# +# Copyright 2013 Google Inc. +# +# 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. + +"""Sample application that demonstrates how to internationalize and localize +and App Engine application. + +For more information, see README.md +""" + +import webapp2 + +from i18n_utils import BaseHandler + + +class MainHandler(BaseHandler): + """A simple handler with internationalized strings. + + This handler demonstrates how to internationalize strings in + Python, Jinja2 template and Javascript. + """ + + def get(self): + """A get handler for this sample. + + It just shows internationalized strings in Python, Jinja2 + template and Javascript. + """ + + context = dict(message=gettext('Hello World from Python code!')) + template = self.jinja2_env.get_template('index.jinja2') + self.response.out.write(template.render(context)) + + +application = webapp2.WSGIApplication([ + ('/', MainHandler), +], debug=True) diff --git a/appengine/standard/i18n/static/js/main.js b/appengine/standard/i18n/static/js/main.js new file mode 100644 index 00000000000..81986d124b0 --- /dev/null +++ b/appengine/standard/i18n/static/js/main.js @@ -0,0 +1,9 @@ + +function init() { + + document.getElementById('js-message').innerHTML = + gettext('Hello World from javascript!') +} + + +window.onload = init; diff --git a/appengine/standard/i18n/templates/i18n_js.jinja2 b/appengine/standard/i18n/templates/i18n_js.jinja2 new file mode 100644 index 00000000000..07f66febac5 --- /dev/null +++ b/appengine/standard/i18n/templates/i18n_js.jinja2 @@ -0,0 +1,38 @@ +{% extends "null_i18n_js.jinja2" %} +{% block i18n_functions -%} +var translations = {{ translations|safe }} +function get_value_from_translations(translations, msgid) { + var ret = translations['catalog'][msgid] + if (typeof(ret) == 'undefined' && + translations['fallback'] != null) { + ret = get_value_from_translations(translations['fallback'], msgid); + } + return ret; +} + +function plural_index(count, translations) { + var s = 'var n = ' + count + '; var v = ' + translations['plural']; + eval(s); + return v; +} + +function gettext(msgid) { + var value = get_value_from_translations(translations, msgid); + + if (typeof(value) == 'undefined') { + return msgid; + } else { + return (typeof(value) == 'string') ? value : value[0]; + } +} + +function ngettext(singular, plural, count) { + var value = get_value_from_translations(translations, singular); + + if (typeof(value) == 'undefined') { + return (count == 1) ? singular : plural; + } else { + return value[plural_index(count, translations)]; + } +} +{%- endblock %} \ No newline at end of file diff --git a/appengine/standard/i18n/templates/index.jinja2 b/appengine/standard/i18n/templates/index.jinja2 new file mode 100644 index 00000000000..dd9687d590c --- /dev/null +++ b/appengine/standard/i18n/templates/index.jinja2 @@ -0,0 +1,13 @@ + + +{{ get_i18n_js_tag() }} +An internationalized message generated in the Python code: {{ message|e }}
    +An internationalized message generated in jinja2 template: +{{ gettext('Hello World from Jinja2 template!')|e }}
    + +An internationalized message generated in javascript: +
    + + + diff --git a/appengine/standard/i18n/templates/javascript_tag.jinja2 b/appengine/standard/i18n/templates/javascript_tag.jinja2 new file mode 100644 index 00000000000..2f7952359dc --- /dev/null +++ b/appengine/standard/i18n/templates/javascript_tag.jinja2 @@ -0,0 +1,3 @@ + diff --git a/appengine/standard/i18n/templates/null_i18n_js.jinja2 b/appengine/standard/i18n/templates/null_i18n_js.jinja2 new file mode 100644 index 00000000000..4c7837320bd --- /dev/null +++ b/appengine/standard/i18n/templates/null_i18n_js.jinja2 @@ -0,0 +1,15 @@ +{% block i18n_functions -%} +function gettext(s) { return s; }; +function ngettext(singular, plural, count) { + return (count == 1) ? singular :plural; +} +{%- endblock %} +String.prototype.format = function() { + var args = arguments; + return this.replace(/{(\d+)}/g, function(match, number) { + return typeof args[number] != 'undefined' + ? args[number] + : match + ; + }); +}; diff --git a/appengine/standard/images/api/README.md b/appengine/standard/images/api/README.md new file mode 100644 index 00000000000..787710dbf86 --- /dev/null +++ b/appengine/standard/images/api/README.md @@ -0,0 +1,13 @@ +## Images Guestbook Sample + +This is a sample app for Google App Engine that demonstrates the [Images Python +API](https://cloud.google.com/appengine/docs/python/images/usingimages). + + +These samples are used on the following documentation page: + +> https://cloud.google.com/appengine/docs/python/images/ + + + +Refer to the [App Engine Samples README](../../README.md) for information on how to run and deploy this sample. diff --git a/appengine/standard/images/api/app.yaml b/appengine/standard/images/api/app.yaml new file mode 100644 index 00000000000..697c4e6d018 --- /dev/null +++ b/appengine/standard/images/api/app.yaml @@ -0,0 +1,8 @@ +runtime: python27 +api_version: 1 +threadsafe: yes + +handlers: + +- url: .* + script: main.app diff --git a/appengine/standard/images/api/blobstore.py b/appengine/standard/images/api/blobstore.py new file mode 100644 index 00000000000..9a838ac68f3 --- /dev/null +++ b/appengine/standard/images/api/blobstore.py @@ -0,0 +1,52 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Sample application that demonstrates how to use the App Engine Images API. + +For more information, see README.md. +""" + +# [START all] +# [START thumbnailer] +from google.appengine.api import images +from google.appengine.ext import blobstore + +import webapp2 + + +class Thumbnailer(webapp2.RequestHandler): + def get(self): + blob_key = self.request.get("blob_key") + if blob_key: + blob_info = blobstore.get(blob_key) + + if blob_info: + img = images.Image(blob_key=blob_key) + img.resize(width=80, height=100) + img.im_feeling_lucky() + thumbnail = img.execute_transforms(output_encoding=images.JPEG) + + self.response.headers['Content-Type'] = 'image/jpeg' + self.response.out.write(thumbnail) + return + + # Either "blob_key" wasn't provided, or there was no value with that ID + # in the Blobstore. + self.error(404) +# [END thumbnailer] + + +app = webapp2.WSGIApplication([('/img', Thumbnailer)], debug=True) +# [END all] diff --git a/appengine/standard/images/api/blobstore_test.py b/appengine/standard/images/api/blobstore_test.py new file mode 100644 index 00000000000..d6b3f4e698f --- /dev/null +++ b/appengine/standard/images/api/blobstore_test.py @@ -0,0 +1,46 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import mock +import pytest +import webtest + +import blobstore + + +@pytest.fixture +def app(testbed): + return webtest.TestApp(blobstore.app) + + +def test_img(app): + with mock.patch('blobstore.images') as mock_images: + with mock.patch('blobstore.blobstore') as mock_blobstore: + mock_blobstore.get.return_value = b'123' + mock_images.resize.return_value = 'asdf' + mock_images.im_feeling_lucky.return_value = 'gsdf' + + response = app.get('/img?blob_key=123') + + assert response.status_int == 200 + + +def test_img_missing(app): + # Bogus blob_key, should get error + app.get('/img?blob_key=123', status=404) + + +def test_no_img_id(app): + # No blob_key, should get error + app.get('/img', status=404) diff --git a/appengine/standard/images/api/favicon.ico b/appengine/standard/images/api/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..23c553a2966ca4cecf146093f33d8114b4f1e368 GIT binary patch literal 8348 zcmeI0du&tJ8Ng3Vwv|ewX4Hu@my&|vCD%DN@CLnxyptBT_%HjatoYr7%N3zGPed#@ckvA><;HWecDU4&2}*2(h%gm&WgCftWj zkQfW;&mXr@0S&(csd+cjaImbXSy=NC<10}z(efGwgi=;B$dt=HkKYCvp-#3KX+qIu zxx$>zcwk+N=c%1^J9)sO44lmS0##~wzU(43f>wW-p_YXwj>s>3{gHI*^oo1lmJ!X*bw{3UBAKRut zUh5Uy4_94IIkdxC^v{_g%FjuEI+f&;9KQ3i+nK7pU_RivFjP_DTl-&gP_qr$nD=M{ z@z;C3#y5Jsh77svGQ4u$ZX)t}=e52{#Xvk;4qIxNh86kR$OhCAie%&e`U{r{ACQZ@ z-)1#sItef{2L za&U;nm+_lo@lbR7vat&^EcECsH)uSnhlht@etsV4LE5B`j#GW*iur~}xpYlTtc;FC{nl|LK^miZ0y3WU>MIM zmc}94VzEFx9?#&?4l+h;ggzvOeCI$ob=`tBp*&EtirH2xU z4a#KB)IMa{_YdYrnxq>-DrrQ>rfpYe8@|Nc-oHnG*L(HR$}d4Eo2&X@o1~9l@%@W) zU{%rv$`tBAOHJH+?zcv7`!T~x;=<#7@4R5T^5$-%Pz-NAYt+5{d;>R+9W24y&`;ZkZqFGQ6_)<35Z_$5V#ga#=N9985-M0KR*fl@h4M0BxWvbYQr6t zULtYBAMJ%iU>x|?U8z_Zyv1jg_VcZ^kO)pd_)jk`_~2MHZmybbloW?l)lnMrb~TAX zV&%#e+VLvM4!%js+%B6}N!=udFlN5Jv;yQm0sdW({6ny+{{$_b`%pI&q3%#p3R?q( z%3@zpafPo4-GA}ErIfU@j?gpfdgeyI);;S-otz(GefQbXvG3J6hbwD_0*YOuqb1V8 zpQm{(oT|g$!tcw;Ck6l>=mo$z0J@0f0^U0{x?+mD8}QB{9yV7qQ;&$5^%*fb@qUB& zX(O<{D-aZ493K;1xH%!}9+}vt8Sshs4os9)d132glTa=lIJv}MJyU_Y!ZFl62j9@l zggg2y{y~c&Vm0cMa@}r@bbn?HR4Sa|66p;nlMl_6D%_%EjCNRq)NBvx!R!t`@zUoW zKV#O#EhZy4>~?VU+rfg@_W_4Kt~zS<|3JjVMcZ#exy;pDUypq|xjpD&0#H{BHgrvM zI=z9nnTN~NCX?mf@DOU;!82-#gXGsw5CSSf22lJ>u8duE&igGuZq4lVZf*LH2%QqOqkv@O{w`Y}q~yWm0EP zeSP~Hau49ZBU@&hWwH5YG0dnGVN1^iztQFh>rIvj5$iQ;q^nyuSuXo`U~{E8DpuIS zA{kR5oC9o=_>jhfRX@QTs1KRm{@F31 zFKP1!w?51@R^I~kA%H*B0W^sKnyVIsv`_2;=zbUGR9pOTqUhV{{^UH=RQ2@S?`uaQ zEy`)Gsd}1IEedW&4lAe0Sg5h`nQXqaZ}RyEzX{FT?hmF3>0_QQT1V~jI$wc&1@aZh ZS0G=3d706zJ{{SaIO|Jj| literal 0 HcmV?d00001 diff --git a/appengine/standard/images/api/main.py b/appengine/standard/images/api/main.py new file mode 100644 index 00000000000..fd9432addfa --- /dev/null +++ b/appengine/standard/images/api/main.py @@ -0,0 +1,56 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Sample application that demonstrates how to use the App Engine Images API. + +For more information, see README.md. +""" + +# [START all] +# [START thumbnailer] +from google.appengine.api import images +from google.appengine.ext import ndb + +import webapp2 + + +class Photo(ndb.Model): + title = ndb.StringProperty() + full_size_image = ndb.BlobProperty() + + +class Thumbnailer(webapp2.RequestHandler): + def get(self): + if self.request.get("id"): + photo = Photo.get_by_id(int(self.request.get("id"))) + + if photo: + img = images.Image(photo.full_size_image) + img.resize(width=80, height=100) + img.im_feeling_lucky() + thumbnail = img.execute_transforms(output_encoding=images.JPEG) + + self.response.headers['Content-Type'] = 'image/jpeg' + self.response.out.write(thumbnail) + return + + # Either "id" wasn't provided, or there was no image with that ID + # in the datastore. + self.error(404) +# [END thumbnailer] + + +app = webapp2.WSGIApplication([('/img', Thumbnailer)], debug=True) +# [END all] diff --git a/appengine/standard/images/api/main_test.py b/appengine/standard/images/api/main_test.py new file mode 100644 index 00000000000..33caf1fa2f0 --- /dev/null +++ b/appengine/standard/images/api/main_test.py @@ -0,0 +1,50 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import mock +import pytest +import webtest + +import main + + +@pytest.fixture +def app(testbed): + return webtest.TestApp(main.app) + + +def test_img(app): + with mock.patch('main.images') as mock_images: + mock_images.resize.return_value = 'asdf' + mock_images.im_feeling_lucky.return_value = 'gsdf' + photo = main.Photo( + id=234 + ) + photo.title = 'asdf' + photo.full_size_image = b'123' + photo.put() + + response = app.get('/img?id=%s' % photo.key.id()) + + assert response.status_int == 200 + + +def test_img_missing(app): + # Bogus image id, should get error + app.get('/img?id=123', status=404) + + +def test_no_img_id(app): + # No image id, should get error + app.get('/img', status=404) diff --git a/appengine/standard/images/guestbook/README.md b/appengine/standard/images/guestbook/README.md new file mode 100644 index 00000000000..99154fab7e7 --- /dev/null +++ b/appengine/standard/images/guestbook/README.md @@ -0,0 +1,13 @@ +## Images Guestbook Sample + +This is a sample app for Google App Engine that demonstrates the [Images Python +API](https://cloud.google.com/appengine/docs/python/images/usingimages). + + +These samples are used on the following documentation page: + +> https://cloud.google.com/appengine/docs/python/images/usingimages + + + +Refer to the [App Engine Samples README](../../README.md) for information on how to run and deploy this sample. diff --git a/appengine/standard/images/guestbook/app.yaml b/appengine/standard/images/guestbook/app.yaml new file mode 100644 index 00000000000..697c4e6d018 --- /dev/null +++ b/appengine/standard/images/guestbook/app.yaml @@ -0,0 +1,8 @@ +runtime: python27 +api_version: 1 +threadsafe: yes + +handlers: + +- url: .* + script: main.app diff --git a/appengine/standard/images/guestbook/favicon.ico b/appengine/standard/images/guestbook/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..23c553a2966ca4cecf146093f33d8114b4f1e368 GIT binary patch literal 8348 zcmeI0du&tJ8Ng3Vwv|ewX4Hu@my&|vCD%DN@CLnxyptBT_%HjatoYr7%N3zGPed#@ckvA><;HWecDU4&2}*2(h%gm&WgCftWj zkQfW;&mXr@0S&(csd+cjaImbXSy=NC<10}z(efGwgi=;B$dt=HkKYCvp-#3KX+qIu zxx$>zcwk+N=c%1^J9)sO44lmS0##~wzU(43f>wW-p_YXwj>s>3{gHI*^oo1lmJ!X*bw{3UBAKRut zUh5Uy4_94IIkdxC^v{_g%FjuEI+f&;9KQ3i+nK7pU_RivFjP_DTl-&gP_qr$nD=M{ z@z;C3#y5Jsh77svGQ4u$ZX)t}=e52{#Xvk;4qIxNh86kR$OhCAie%&e`U{r{ACQZ@ z-)1#sItef{2L za&U;nm+_lo@lbR7vat&^EcECsH)uSnhlht@etsV4LE5B`j#GW*iur~}xpYlTtc;FC{nl|LK^miZ0y3WU>MIM zmc}94VzEFx9?#&?4l+h;ggzvOeCI$ob=`tBp*&EtirH2xU z4a#KB)IMa{_YdYrnxq>-DrrQ>rfpYe8@|Nc-oHnG*L(HR$}d4Eo2&X@o1~9l@%@W) zU{%rv$`tBAOHJH+?zcv7`!T~x;=<#7@4R5T^5$-%Pz-NAYt+5{d;>R+9W24y&`;ZkZqFGQ6_)<35Z_$5V#ga#=N9985-M0KR*fl@h4M0BxWvbYQr6t zULtYBAMJ%iU>x|?U8z_Zyv1jg_VcZ^kO)pd_)jk`_~2MHZmybbloW?l)lnMrb~TAX zV&%#e+VLvM4!%js+%B6}N!=udFlN5Jv;yQm0sdW({6ny+{{$_b`%pI&q3%#p3R?q( z%3@zpafPo4-GA}ErIfU@j?gpfdgeyI);;S-otz(GefQbXvG3J6hbwD_0*YOuqb1V8 zpQm{(oT|g$!tcw;Ck6l>=mo$z0J@0f0^U0{x?+mD8}QB{9yV7qQ;&$5^%*fb@qUB& zX(O<{D-aZ493K;1xH%!}9+}vt8Sshs4os9)d132glTa=lIJv}MJyU_Y!ZFl62j9@l zggg2y{y~c&Vm0cMa@}r@bbn?HR4Sa|66p;nlMl_6D%_%EjCNRq)NBvx!R!t`@zUoW zKV#O#EhZy4>~?VU+rfg@_W_4Kt~zS<|3JjVMcZ#exy;pDUypq|xjpD&0#H{BHgrvM zI=z9nnTN~NCX?mf@DOU;!82-#gXGsw5CSSf22lJ>u8duE&igGuZq4lVZf*LH2%QqOqkv@O{w`Y}q~yWm0EP zeSP~Hau49ZBU@&hWwH5YG0dnGVN1^iztQFh>rIvj5$iQ;q^nyuSuXo`U~{E8DpuIS zA{kR5oC9o=_>jhfRX@QTs1KRm{@F31 zFKP1!w?51@R^I~kA%H*B0W^sKnyVIsv`_2;=zbUGR9pOTqUhV{{^UH=RQ2@S?`uaQ zEy`)Gsd}1IEedW&4lAe0Sg5h`nQXqaZ}RyEzX{FT?hmF3>0_QQT1V~jI$wc&1@aZh ZS0G=3d706zJ{{SaIO|Jj| literal 0 HcmV?d00001 diff --git a/appengine/standard/images/guestbook/index.yaml b/appengine/standard/images/guestbook/index.yaml new file mode 100644 index 00000000000..f933a1c8e6d --- /dev/null +++ b/appengine/standard/images/guestbook/index.yaml @@ -0,0 +1,17 @@ +indexes: + +# AUTOGENERATED + +# This index.yaml is automatically updated whenever the dev_appserver +# detects that a new type of query is run. If you want to manage the +# index.yaml file manually, remove the above marker line (the line +# saying "# AUTOGENERATED"). If you want to manage some indexes +# manually, move them above the marker line. The index.yaml file is +# automatically uploaded to the admin console when you next deploy +# your application using appcfg.py. + +- kind: Greeting + ancestor: yes + properties: + - name: date + direction: desc diff --git a/appengine/standard/images/guestbook/main.py b/appengine/standard/images/guestbook/main.py new file mode 100644 index 00000000000..9a66a82f8b7 --- /dev/null +++ b/appengine/standard/images/guestbook/main.py @@ -0,0 +1,138 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Sample application that demonstrates how to use the App Engine Images API. + +For more information, see README.md. +""" + +# [START all] + +import cgi +import urllib + +# [START import_images] +from google.appengine.api import images +# [END import_images] +from google.appengine.api import users +from google.appengine.ext import ndb + +import webapp2 + + +# [START model] +class Greeting(ndb.Model): + """Models a Guestbook entry with an author, content, avatar, and date.""" + author = ndb.StringProperty() + content = ndb.TextProperty() + avatar = ndb.BlobProperty() + date = ndb.DateTimeProperty(auto_now_add=True) +# [END model] + + +def guestbook_key(guestbook_name=None): + """Constructs a Datastore key for a Guestbook entity with name.""" + return ndb.Key('Guestbook', guestbook_name or 'default_guestbook') + + +class MainPage(webapp2.RequestHandler): + def get(self): + self.response.out.write('') + guestbook_name = self.request.get('guestbook_name') + + greetings = Greeting.query( + ancestor=guestbook_key(guestbook_name)) \ + .order(-Greeting.date) \ + .fetch(10) + + for greeting in greetings: + if greeting.author: + self.response.out.write( + '%s wrote:' % greeting.author) + else: + self.response.out.write('An anonymous person wrote:') + # [START display_image] + self.response.out.write('
    ' % + greeting.key.urlsafe()) + self.response.out.write('
    %s
    ' % + cgi.escape(greeting.content)) + # [END display_image] + + # [START form] + self.response.out.write(""" +
    +
    + +
    +
    +
    +
    +
    +
    +
    Guestbook name: +
    + + """ % (urllib.urlencode({'guestbook_name': guestbook_name}), + cgi.escape(guestbook_name))) + # [END form] + + +# [START image_handler] +class Image(webapp2.RequestHandler): + def get(self): + greeting_key = ndb.Key(urlsafe=self.request.get('img_id')) + greeting = greeting_key.get() + if greeting.avatar: + self.response.headers['Content-Type'] = 'image/png' + self.response.out.write(greeting.avatar) + else: + self.response.out.write('No image') +# [END image_handler] + + +# [START sign_handler] +class Guestbook(webapp2.RequestHandler): + def post(self): + guestbook_name = self.request.get('guestbook_name') + greeting = Greeting(parent=guestbook_key(guestbook_name)) + + if users.get_current_user(): + greeting.author = users.get_current_user().nickname() + + greeting.content = self.request.get('content') + + # [START sign_handler_1] + avatar = self.request.get('img') + # [END sign_handler_1] + # [START transform] + avatar = images.resize(avatar, 32, 32) + # [END transform] + # [START sign_handler_2] + greeting.avatar = avatar + greeting.put() + # [END sign_handler_1] + + self.redirect('/?' + urllib.urlencode( + {'guestbook_name': guestbook_name})) +# [END sign_handler] + + +app = webapp2.WSGIApplication([('/', MainPage), + ('/img', Image), + ('/sign', Guestbook)], + debug=True) +# [END all] diff --git a/appengine/standard/images/guestbook/main_test.py b/appengine/standard/images/guestbook/main_test.py new file mode 100644 index 00000000000..8ef5dfc7339 --- /dev/null +++ b/appengine/standard/images/guestbook/main_test.py @@ -0,0 +1,78 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import mock +import pytest +import webtest + +import main + + +@pytest.fixture +def app(testbed): + return webtest.TestApp(main.app) + + +def test_get(app): + main.Greeting( + parent=main.guestbook_key('default_guestbook'), + author='123', + content='abc' + ).put() + + response = app.get('/') + + # Let's check if the response is correct. + assert response.status_int == 200 + + +def test_post(app): + with mock.patch('main.images') as mock_images: + mock_images.resize.return_value = 'asdf' + + response = app.post('/sign', {'content': 'asdf'}) + mock_images.resize.assert_called_once_with(mock.ANY, 32, 32) + + # Correct response is a redirect + assert response.status_int == 302 + + +def test_img(app): + greeting = main.Greeting( + parent=main.guestbook_key('default_guestbook'), + id=123 + ) + greeting.author = 'asdf' + greeting.content = 'asdf' + greeting.avatar = b'123' + greeting.put() + + response = app.get('/img?img_id=%s' % greeting.key.urlsafe()) + + assert response.status_int == 200 + + +def test_img_missing(app): + # Bogus image id, should get error + app.get('/img?img_id=123', status=500) + + +def test_post_and_get(app): + with mock.patch('main.images') as mock_images: + mock_images.resize.return_value = 'asdf' + + app.post('/sign', {'content': 'asdf'}) + response = app.get('/') + + assert response.status_int == 200 diff --git a/appengine/standard/localtesting/README.md b/appengine/standard/localtesting/README.md new file mode 100644 index 00000000000..0d45442f02b --- /dev/null +++ b/appengine/standard/localtesting/README.md @@ -0,0 +1,10 @@ +# App Engine Local Testing Samples + +These samples show how to do automated testing of App Engine applications. + + +These samples are used on the following documentation page: + +> https://cloud.google.com/appengine/docs/python/tools/localunittesting + + diff --git a/appengine/standard/localtesting/datastore_test.py b/appengine/standard/localtesting/datastore_test.py new file mode 100644 index 00000000000..8a505d05b1a --- /dev/null +++ b/appengine/standard/localtesting/datastore_test.py @@ -0,0 +1,165 @@ +# Copyright 2015 Google Inc +# +# 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 imports] +import unittest + +from google.appengine.api import memcache +from google.appengine.ext import ndb +from google.appengine.ext import testbed +# [END imports] + + +# [START datastore_example_1] +class TestModel(ndb.Model): + """A model class used for testing.""" + number = ndb.IntegerProperty(default=42) + text = ndb.StringProperty() + + +class TestEntityGroupRoot(ndb.Model): + """Entity group root""" + pass + + +def GetEntityViaMemcache(entity_key): + """Get entity from memcache if available, from datastore if not.""" + entity = memcache.get(entity_key) + if entity is not None: + return entity + key = ndb.Key(urlsafe=entity_key) + entity = key.get() + if entity is not None: + memcache.set(entity_key, entity) + return entity +# [END datastore_example_1] + + +# [START datastore_example_test] +class DatastoreTestCase(unittest.TestCase): + + def setUp(self): + # First, create an instance of the Testbed class. + self.testbed = testbed.Testbed() + # Then activate the testbed, which prepares the service stubs for use. + self.testbed.activate() + # Next, declare which service stubs you want to use. + self.testbed.init_datastore_v3_stub() + self.testbed.init_memcache_stub() + # Clear ndb's in-context cache between tests. + # This prevents data from leaking between tests. + # Alternatively, you could disable caching by + # using ndb.get_context().set_cache_policy(False) + ndb.get_context().clear_cache() + +# [END datastore_example_test] + + # [START datastore_example_teardown] + def tearDown(self): + self.testbed.deactivate() + # [END datastore_example_teardown] + + # [START datastore_example_insert] + def testInsertEntity(self): + TestModel().put() + self.assertEqual(1, len(TestModel.query().fetch(2))) + # [END datastore_example_insert] + + # [START datastore_example_filter] + def testFilterByNumber(self): + root = TestEntityGroupRoot(id="root") + TestModel(parent=root.key).put() + TestModel(number=17, parent=root.key).put() + query = TestModel.query(ancestor=root.key).filter( + TestModel.number == 42) + results = query.fetch(2) + self.assertEqual(1, len(results)) + self.assertEqual(42, results[0].number) + # [END datastore_example_filter] + + # [START datastore_example_memcache] + def testGetEntityViaMemcache(self): + entity_key = TestModel(number=18).put().urlsafe() + retrieved_entity = GetEntityViaMemcache(entity_key) + self.assertNotEqual(None, retrieved_entity) + self.assertEqual(18, retrieved_entity.number) + # [END datastore_example_memcache] + + +# [START HRD_example_1] +from google.appengine.datastore import datastore_stub_util # noqa + + +class HighReplicationTestCaseOne(unittest.TestCase): + + def setUp(self): + # First, create an instance of the Testbed class. + self.testbed = testbed.Testbed() + # Then activate the testbed, which prepares the service stubs for use. + self.testbed.activate() + # Create a consistency policy that will simulate the High Replication + # consistency model. + self.policy = datastore_stub_util.PseudoRandomHRConsistencyPolicy( + probability=0) + # Initialize the datastore stub with this policy. + self.testbed.init_datastore_v3_stub(consistency_policy=self.policy) + # Initialize memcache stub too, since ndb also uses memcache + self.testbed.init_memcache_stub() + # Clear in-context cache before each test. + ndb.get_context().clear_cache() + + def tearDown(self): + self.testbed.deactivate() + + def testEventuallyConsistentGlobalQueryResult(self): + class TestModel(ndb.Model): + pass + + user_key = ndb.Key('User', 'ryan') + + # Put two entities + ndb.put_multi([ + TestModel(parent=user_key), + TestModel(parent=user_key) + ]) + + # Global query doesn't see the data. + self.assertEqual(0, TestModel.query().count(3)) + # Ancestor query does see the data. + self.assertEqual(2, TestModel.query(ancestor=user_key).count(3)) +# [END HRD_example_1] + + # [START HRD_example_2] + def testDeterministicOutcome(self): + # 50% chance to apply. + self.policy.SetProbability(.5) + # Use the pseudo random sequence derived from seed=2. + self.policy.SetSeed(2) + + class TestModel(ndb.Model): + pass + + TestModel().put() + + self.assertEqual(0, TestModel.query().count(3)) + self.assertEqual(0, TestModel.query().count(3)) + # Will always be applied before the third query. + self.assertEqual(1, TestModel.query().count(3)) + # [END HRD_example_2] + + +# [START main] +if __name__ == '__main__': + unittest.main() +# [END main] diff --git a/appengine/standard/localtesting/env_vars_test.py b/appengine/standard/localtesting/env_vars_test.py new file mode 100644 index 00000000000..31e374def64 --- /dev/null +++ b/appengine/standard/localtesting/env_vars_test.py @@ -0,0 +1,41 @@ +# Copyright 2015 Google Inc +# +# 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 env_example] +import os +import unittest + +from google.appengine.ext import testbed + + +class EnvVarsTestCase(unittest.TestCase): + def setUp(self): + self.testbed = testbed.Testbed() + self.testbed.activate() + self.testbed.setup_env( + app_id='your-app-id', + my_config_setting='example', + overwrite=True) + + def tearDown(self): + self.testbed.deactivate() + + def testEnvVars(self): + self.assertEqual(os.environ['APPLICATION_ID'], 'your-app-id') + self.assertEqual(os.environ['MY_CONFIG_SETTING'], 'example') +# [END env_example] + + +if __name__ == '__main__': + unittest.main() diff --git a/appengine/standard/localtesting/login_test.py b/appengine/standard/localtesting/login_test.py new file mode 100644 index 00000000000..8886b8cb6e6 --- /dev/null +++ b/appengine/standard/localtesting/login_test.py @@ -0,0 +1,54 @@ +# Copyright 2015 Google Inc +# +# 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 login_example] +import unittest + +from google.appengine.api import users +from google.appengine.ext import testbed + + +class LoginTestCase(unittest.TestCase): + # [START setup] + def setUp(self): + self.testbed = testbed.Testbed() + self.testbed.activate() + self.testbed.init_user_stub() + # [END setup] + + def tearDown(self): + self.testbed.deactivate() + + # [START login] + def loginUser(self, email='user@example.com', id='123', is_admin=False): + self.testbed.setup_env( + user_email=email, + user_id=id, + user_is_admin='1' if is_admin else '0', + overwrite=True) + # [END login] + + # [START test] + def testLogin(self): + self.assertFalse(users.get_current_user()) + self.loginUser() + self.assertEquals(users.get_current_user().email(), 'user@example.com') + self.loginUser(is_admin=True) + self.assertTrue(users.is_current_user_admin()) + # [END test] +# [END login_example] + + +if __name__ == '__main__': + unittest.main() diff --git a/appengine/standard/localtesting/mail_test.py b/appengine/standard/localtesting/mail_test.py new file mode 100644 index 00000000000..a362aa210eb --- /dev/null +++ b/appengine/standard/localtesting/mail_test.py @@ -0,0 +1,45 @@ +# Copyright 2015 Google Inc +# +# 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 mail_example] +import unittest + +from google.appengine.api import mail +from google.appengine.ext import testbed + + +class MailTestCase(unittest.TestCase): + + def setUp(self): + self.testbed = testbed.Testbed() + self.testbed.activate() + self.testbed.init_mail_stub() + self.mail_stub = self.testbed.get_stub(testbed.MAIL_SERVICE_NAME) + + def tearDown(self): + self.testbed.deactivate() + + def testMailSent(self): + mail.send_mail(to='alice@example.com', + subject='This is a test', + sender='bob@example.com', + body='This is a test e-mail') + messages = self.mail_stub.get_sent_messages(to='alice@example.com') + self.assertEqual(1, len(messages)) + self.assertEqual('alice@example.com', messages[0].to) +# [END mail_example] + + +if __name__ == '__main__': + unittest.main() diff --git a/appengine/standard/localtesting/queue.yaml b/appengine/standard/localtesting/queue.yaml new file mode 100644 index 00000000000..fcdd5444dd9 --- /dev/null +++ b/appengine/standard/localtesting/queue.yaml @@ -0,0 +1,7 @@ +queue: +- name: default + rate: 5/s +- name: queue-1 + rate: 5/s +- name: queue-2 + rate: 5/s diff --git a/appengine/standard/localtesting/resources/queue.yaml b/appengine/standard/localtesting/resources/queue.yaml new file mode 100644 index 00000000000..fcdd5444dd9 --- /dev/null +++ b/appengine/standard/localtesting/resources/queue.yaml @@ -0,0 +1,7 @@ +queue: +- name: default + rate: 5/s +- name: queue-1 + rate: 5/s +- name: queue-2 + rate: 5/s diff --git a/appengine/standard/localtesting/runner.py b/appengine/standard/localtesting/runner.py new file mode 100755 index 00000000000..6cd1aaca7b2 --- /dev/null +++ b/appengine/standard/localtesting/runner.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python2 + +# Copyright 2015 Google Inc +# +# 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 runner] +"""App Engine local test runner example. + +This program handles properly importing the App Engine SDK so that test modules +can use google.appengine.* APIs and the Google App Engine testbed. + +Example invocation: + + $ python runner.py ~/google-cloud-sdk +""" + +import argparse +import os +import sys +import unittest + + +def fixup_paths(path): + """Adds GAE SDK path to system path and appends it to the google path + if that already exists.""" + # Not all Google packages are inside namespace packages, which means + # there might be another non-namespace package named `google` already on + # the path and simply appending the App Engine SDK to the path will not + # work since the other package will get discovered and used first. + # This emulates namespace packages by first searching if a `google` package + # exists by importing it, and if so appending to its module search path. + try: + import google + google.__path__.append("{0}/google".format(path)) + except ImportError: + pass + + sys.path.insert(0, path) + + +def main(sdk_path, test_path, test_pattern): + # If the SDK path points to a Google Cloud SDK installation + # then we should alter it to point to the GAE platform location. + if os.path.exists(os.path.join(sdk_path, 'platform/google_appengine')): + sdk_path = os.path.join(sdk_path, 'platform/google_appengine') + + # Make sure google.appengine.* modules are importable. + fixup_paths(sdk_path) + + # Make sure all bundled third-party packages are available. + import dev_appserver + dev_appserver.fix_sys_path() + + # Loading appengine_config from the current project ensures that any + # changes to configuration there are available to all tests (e.g. + # sys.path modifications, namespaces, etc.) + try: + import appengine_config + (appengine_config) + except ImportError: + print('Note: unable to import appengine_config.') + + # Discover and run tests. + suite = unittest.loader.TestLoader().discover(test_path, test_pattern) + return unittest.TextTestRunner(verbosity=2).run(suite) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument( + 'sdk_path', + help='The path to the Google App Engine SDK or the Google Cloud SDK.') + parser.add_argument( + '--test-path', + help='The path to look for tests, defaults to the current directory.', + default=os.getcwd()) + parser.add_argument( + '--test-pattern', + help='The file pattern for test modules, defaults to *_test.py.', + default='*_test.py') + + args = parser.parse_args() + + result = main(args.sdk_path, args.test_path, args.test_pattern) + + if not result.wasSuccessful(): + sys.exit(1) + +# [END runner] diff --git a/appengine/standard/localtesting/task_queue_test.py b/appengine/standard/localtesting/task_queue_test.py new file mode 100644 index 00000000000..2472fe18887 --- /dev/null +++ b/appengine/standard/localtesting/task_queue_test.py @@ -0,0 +1,90 @@ +# Copyright 2015 Google Inc +# +# 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 taskqueue] +import operator +import os +import unittest + +from google.appengine.api import taskqueue +from google.appengine.ext import deferred +from google.appengine.ext import testbed + + +class TaskQueueTestCase(unittest.TestCase): + def setUp(self): + self.testbed = testbed.Testbed() + self.testbed.activate() + + # root_path must be set the the location of queue.yaml. + # Otherwise, only the 'default' queue will be available. + self.testbed.init_taskqueue_stub( + root_path=os.path.join(os.path.dirname(__file__), 'resources')) + self.taskqueue_stub = self.testbed.get_stub( + testbed.TASKQUEUE_SERVICE_NAME) + + def tearDown(self): + self.testbed.deactivate() + + def testTaskAddedToQueue(self): + taskqueue.Task(name='my_task', url='/url/of/my/task/').add() + tasks = self.taskqueue_stub.get_filtered_tasks() + self.assertEqual(len(tasks), 1) + self.assertEqual(tasks[0].name, 'my_task') +# [END taskqueue] + + # [START filtering] + def testFiltering(self): + taskqueue.Task(name='task_one', url='/url/of/task/1/').add('queue-1') + taskqueue.Task(name='task_two', url='/url/of/task/2/').add('queue-2') + + # All tasks + tasks = self.taskqueue_stub.get_filtered_tasks() + self.assertEqual(len(tasks), 2) + + # Filter by name + tasks = self.taskqueue_stub.get_filtered_tasks(name='task_one') + self.assertEqual(len(tasks), 1) + self.assertEqual(tasks[0].name, 'task_one') + + # Filter by URL + tasks = self.taskqueue_stub.get_filtered_tasks(url='/url/of/task/1/') + self.assertEqual(len(tasks), 1) + self.assertEqual(tasks[0].name, 'task_one') + + # Filter by queue + tasks = self.taskqueue_stub.get_filtered_tasks(queue_names='queue-1') + self.assertEqual(len(tasks), 1) + self.assertEqual(tasks[0].name, 'task_one') + + # Multiple queues + tasks = self.taskqueue_stub.get_filtered_tasks( + queue_names=['queue-1', 'queue-2']) + self.assertEqual(len(tasks), 2) + # [END filtering] + + # [START deferred] + def testTaskAddedByDeferred(self): + deferred.defer(operator.add, 1, 2) + + tasks = self.taskqueue_stub.get_filtered_tasks() + self.assertEqual(len(tasks), 1) + + result = deferred.run(tasks[0].payload) + self.assertEqual(result, 3) + # [END deferred] + + +if __name__ == '__main__': + unittest.main() diff --git a/appengine/standard/logging/reading_logs/app.yaml b/appengine/standard/logging/reading_logs/app.yaml new file mode 100644 index 00000000000..42ad35ed2a8 --- /dev/null +++ b/appengine/standard/logging/reading_logs/app.yaml @@ -0,0 +1,7 @@ +runtime: python27 +threadsafe: yes +api_version: 1 + +handlers: +- url: .* + script: main.app diff --git a/appengine/standard/logging/reading_logs/main.py b/appengine/standard/logging/reading_logs/main.py new file mode 100644 index 00000000000..14ed923fde3 --- /dev/null +++ b/appengine/standard/logging/reading_logs/main.py @@ -0,0 +1,103 @@ +# Copyright 2015 Google Inc. +# +# 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. + +""" +Sample Google App Engine application that demonstrates how to use the App +Engine Log Service API to read application logs. +""" + +# [START all] +import base64 +import datetime +from itertools import islice +from textwrap import dedent +import time + +from google.appengine.api.logservice import logservice +import webapp2 + + +def get_logs(offset=None): + # Logs are read backwards from the given end time. This specifies to read + # all logs up until now. + end_time = time.time() + + logs = logservice.fetch( + end_time=end_time, + offset=offset, + minimum_log_level=logservice.LOG_LEVEL_INFO, + include_app_logs=True) + + return logs + + +def format_log_entry(entry): + # Format any application logs that happened during this request. + logs = [] + for log in entry.app_logs: + date = datetime.datetime.fromtimestamp( + log.time).strftime('%D %T UTC') + logs.append('Date: {}, Message: {}'.format( + date, log.message)) + + # Format the request log and include the application logs. + date = datetime.datetime.fromtimestamp( + entry.end_time).strftime('%D %T UTC') + + output = dedent(""" + Date: {} + IP: {} + Method: {} + Resource: {} + Logs: + """.format(date, entry.ip, entry.method, entry.resource)) + + output += '\n'.join(logs) + + return output + + +class MainPage(webapp2.RequestHandler): + def get(self): + offset = self.request.get('offset', None) + + if offset: + offset = base64.urlsafe_b64decode(str(offset)) + + # Get the logs given the specified offset. + logs = get_logs(offset=offset) + + # Output the first 10 logs. + log = None + for log in islice(logs, 10): + self.response.write( + '
    {}
    '.format(format_log_entry(log))) + + offset = log.offset + + if not log: + self.response.write('No log entries found.') + + # Add a link to view more log entries. + elif offset: + self.response.write( + '
    More'.format( + base64.urlsafe_b64encode(offset))) + + +app = webapp2.WSGIApplication([ + ('/', MainPage) +], debug=True) + +# [END all] diff --git a/appengine/standard/logging/reading_logs/main_test.py b/appengine/standard/logging/reading_logs/main_test.py new file mode 100644 index 00000000000..03476add6ec --- /dev/null +++ b/appengine/standard/logging/reading_logs/main_test.py @@ -0,0 +1,25 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import webtest + +import main + + +def test_app(testbed): + app = webtest.TestApp(main.app) + response = app.get('/') + assert response.status_int == 200 + assert 'No log entries found' in response.text + assert 'More' not in response.text diff --git a/appengine/standard/logging/writing_logs/app.yaml b/appengine/standard/logging/writing_logs/app.yaml new file mode 100644 index 00000000000..42ad35ed2a8 --- /dev/null +++ b/appengine/standard/logging/writing_logs/app.yaml @@ -0,0 +1,7 @@ +runtime: python27 +threadsafe: yes +api_version: 1 + +handlers: +- url: .* + script: main.app diff --git a/appengine/standard/logging/writing_logs/main.py b/appengine/standard/logging/writing_logs/main.py new file mode 100644 index 00000000000..442e7710020 --- /dev/null +++ b/appengine/standard/logging/writing_logs/main.py @@ -0,0 +1,47 @@ +# Copyright 2015 Google Inc. +# +# 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. + +""" +Sample Google App Engine application that demonstrates using the standard +Python logging package. Logs are automatically collected and available via +the Google Cloud Console. +""" + +# [START all] +import logging + +import webapp2 + + +class MainPage(webapp2.RequestHandler): + def get(self): + logging.debug('This is a debug message') + logging.info('This is an info message') + logging.warning('This is a warning message') + logging.error('This is an error message') + logging.critical('This is a critical message') + + try: + raise ValueError('This is a sample value error.') + except ValueError: + logging.exception('A example exception log.') + + self.response.out.write('Logging example.') + + +app = webapp2.WSGIApplication([ + ('/', MainPage) +], debug=True) + +# [END all] diff --git a/appengine/standard/logging/writing_logs/main_test.py b/appengine/standard/logging/writing_logs/main_test.py new file mode 100644 index 00000000000..181f788d33a --- /dev/null +++ b/appengine/standard/logging/writing_logs/main_test.py @@ -0,0 +1,24 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import webtest + +import main + + +def test_app(testbed): + app = webtest.TestApp(main.app) + response = app.get('/') + assert response.status_int == 200 + assert 'Logging example' in response.text diff --git a/appengine/standard/mail/README.md b/appengine/standard/mail/README.md new file mode 100644 index 00000000000..f58388c6dec --- /dev/null +++ b/appengine/standard/mail/README.md @@ -0,0 +1,17 @@ +## App Engine Email Docs Snippets + +This sample application demonstrates different ways to send and receive email +on App Engine + + + +These samples are used on the following documentation pages: + +> +* https://cloud.google.com/appengine/docs/python/mail/headers +* https://cloud.google.com/appengine/docs/python/mail/receiving-mail-with-mail-api +* https://cloud.google.com/appengine/docs/python/mail/sending-mail-with-mail-api +* https://cloud.google.com/appengine/docs/python/mail/attachments +* https://cloud.google.com/appengine/docs/python/mail/bounce + + diff --git a/appengine/standard/mail/app.yaml b/appengine/standard/mail/app.yaml new file mode 100644 index 00000000000..1d254490b55 --- /dev/null +++ b/appengine/standard/mail/app.yaml @@ -0,0 +1,47 @@ +runtime: python27 +api_version: 1 +threadsafe: yes + +# [START bounce_service] +# [START mail_service] +inbound_services: +- mail +# [END mail_service] +- mail_bounce +# [END bounce_service] + +handlers: +- url: /user/.+ + script: user_signup.app +- url: /send_mail + script: send_mail.app +- url: /send_message + script: send_message.app +# [START handle_incoming_email] +- url: /_ah/mail/.+ + script: handle_incoming_email.app + login: admin +# [END handle_incoming_email] +# [START handle_all_email] +- url: /_ah/mail/owner@.*your_app_id\.appspotmail\.com + script: handle_owner.app + login: admin +- url: /_ah/mail/support@.*your_app_id\.appspotmail\.com + script: handle_support.app + login: admin +- url: /_ah/mail/.+ + script: handle_catchall.app + login: admin +# [END handle_all_email] +# [START handle_bounced_email] +- url: /_ah/bounce + script: handle_bounced_email.app + login: admin +# [END handle_bounced_email] +- url: /attachment + script: attachment.app +- url: /header + script: header.app +- url: / + static_files: index.html + upload: index.html diff --git a/appengine/standard/mail/attachment.py b/appengine/standard/mail/attachment.py new file mode 100644 index 00000000000..8717d0b1cb1 --- /dev/null +++ b/appengine/standard/mail/attachment.py @@ -0,0 +1,50 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from google.appengine.api import app_identity +from google.appengine.api import mail +import webapp2 + + +# [START send_attachment] +class AttachmentHandler(webapp2.RequestHandler): + def post(self): + f = self.request.POST['file'] + mail.send_mail(sender='{}@appspot.gserviceaccount.com'.format( + app_identity.get_application_id()), + to="Albert Johnson ", + subject="The doc you requested", + body=""" +Attached is the document file you requested. + +The example.com Team +""", + attachments=[(f.filename, f.file.read())]) +# [END send_attachment] + self.response.content_type = 'text/plain' + self.response.write('Sent {} to Albert.'.format(f.filename)) + + def get(self): + self.response.content_type = 'text/html' + self.response.write(""" +
    + Send a file to Albert:
    +

    + +
    + Enter an email thread id: + +
    """) + + def post(self): + print repr(self.request.POST) + id = self.request.POST['thread_id'] + send_example_mail('{}@appspot.gserviceaccount.com'.format( + app_identity.get_application_id()), id) + self.response.content_type = 'text/plain' + self.response.write( + 'Sent an email to Albert with Reference header set to {}.' + .format(id)) + + +app = webapp2.WSGIApplication([ + ('/header', SendMailHandler), +], debug=True) diff --git a/appengine/standard/mail/header_test.py b/appengine/standard/mail/header_test.py new file mode 100644 index 00000000000..da514ef0e72 --- /dev/null +++ b/appengine/standard/mail/header_test.py @@ -0,0 +1,27 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import webtest + +import header + + +def test_send_mail(testbed): + testbed.init_mail_stub() + testbed.init_app_identity_stub() + app = webtest.TestApp(header.app) + response = app.post('/header', 'thread_id=42') + assert response.status_int == 200 + assert ('Sent an email to Albert with Reference header set to 42.' + in response.body) diff --git a/appengine/standard/mail/index.html b/appengine/standard/mail/index.html new file mode 100644 index 00000000000..c0b346c2576 --- /dev/null +++ b/appengine/standard/mail/index.html @@ -0,0 +1,29 @@ + + + + + + Google App Engine Mail Samples + + +

    Send email.

    +

    Send email with a message object.

    +

    Confirm a user's email address.

    +

    Send email with attachments.

    +

    Send email with headers.

    + + diff --git a/appengine/standard/mail/send_mail.py b/appengine/standard/mail/send_mail.py new file mode 100644 index 00000000000..81666b68c37 --- /dev/null +++ b/appengine/standard/mail/send_mail.py @@ -0,0 +1,48 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from google.appengine.api import app_identity +from google.appengine.api import mail +import webapp2 + + +def send_approved_mail(sender_address): + # [START send_mail] + mail.send_mail(sender=sender_address, + to="Albert Johnson ", + subject="Your account has been approved", + body="""Dear Albert: + +Your example.com account has been approved. You can now visit +http://www.example.com/ and sign in using your Google Account to +access new features. + +Please let us know if you have any questions. + +The example.com Team +""") + # [END send_mail] + + +class SendMailHandler(webapp2.RequestHandler): + def get(self): + send_approved_mail('{}@appspot.gserviceaccount.com'.format( + app_identity.get_application_id())) + self.response.content_type = 'text/plain' + self.response.write('Sent an email to Albert.') + + +app = webapp2.WSGIApplication([ + ('/send_mail', SendMailHandler), +], debug=True) diff --git a/appengine/standard/mail/send_mail_test.py b/appengine/standard/mail/send_mail_test.py new file mode 100644 index 00000000000..5e5f7e6be02 --- /dev/null +++ b/appengine/standard/mail/send_mail_test.py @@ -0,0 +1,26 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import webtest + +import send_mail + + +def test_send_mail(testbed): + testbed.init_mail_stub() + testbed.init_app_identity_stub() + app = webtest.TestApp(send_mail.app) + response = app.get('/send_mail') + assert response.status_int == 200 + assert 'Sent an email to Albert.' in response.body diff --git a/appengine/standard/mail/send_message.py b/appengine/standard/mail/send_message.py new file mode 100644 index 00000000000..544c057a38d --- /dev/null +++ b/appengine/standard/mail/send_message.py @@ -0,0 +1,51 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from google.appengine.api import app_identity +from google.appengine.api import mail +import webapp2 + + +def send_approved_mail(sender_address): + # [START send_message] + message = mail.EmailMessage( + sender=sender_address, + subject="Your account has been approved") + + message.to = "Albert Johnson " + message.body = """Dear Albert: + +Your example.com account has been approved. You can now visit +http://www.example.com/ and sign in using your Google Account to +access new features. + +Please let us know if you have any questions. + +The example.com Team +""" + message.send() + # [END send_message] + + +class SendMessageHandler(webapp2.RequestHandler): + def get(self): + send_approved_mail('{}@appspot.gserviceaccount.com'.format( + app_identity.get_application_id())) + self.response.content_type = 'text/plain' + self.response.write('Sent an email message to Albert.') + + +app = webapp2.WSGIApplication([ + ('/send_message', SendMessageHandler), +], debug=True) diff --git a/appengine/standard/mail/send_message_test.py b/appengine/standard/mail/send_message_test.py new file mode 100644 index 00000000000..d56f58e0d6f --- /dev/null +++ b/appengine/standard/mail/send_message_test.py @@ -0,0 +1,26 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import webtest + +import send_message + + +def test_send_message(testbed): + testbed.init_mail_stub() + testbed.init_app_identity_stub() + app = webtest.TestApp(send_message.app) + response = app.get('/send_message') + assert response.status_int == 200 + assert 'Sent an email message to Albert.' in response.body diff --git a/appengine/standard/mail/user_signup.py b/appengine/standard/mail/user_signup.py new file mode 100644 index 00000000000..38b05bd2ca4 --- /dev/null +++ b/appengine/standard/mail/user_signup.py @@ -0,0 +1,106 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import datetime +import random +import socket +import string + +from google.appengine.api import app_identity +from google.appengine.api import mail +from google.appengine.ext import ndb +import webapp2 + + +# [START send-confirm-email] +class UserSignupHandler(webapp2.RequestHandler): + """Serves the email address sign up form.""" + + def post(self): + user_address = self.request.get('email_address') + + if not mail.is_email_valid(user_address): + self.get() # Show the form again. + else: + confirmation_url = create_new_user_confirmation(user_address) + sender_address = ( + 'Example.com Support <{}@appspot.gserviceaccount.com>'.format( + app_identity.get_application_id())) + subject = 'Confirm your registration' + body = """Thank you for creating an account! +Please confirm your email address by clicking on the link below: + +{} +""".format(confirmation_url) + mail.send_mail(sender_address, user_address, subject, body) +# [END send-confirm-email] + self.response.content_type = 'text/plain' + self.response.write('An email has been sent to {}.'.format( + user_address)) + + def get(self): + self.response.content_type = 'text/html' + self.response.write("""
    + Enter your email address: + +
    """) + + +class UserConfirmationRecord(ndb.Model): + """Datastore record with email address and confirmation code.""" + user_address = ndb.StringProperty(indexed=False) + confirmed = ndb.BooleanProperty(indexed=False, default=False) + timestamp = ndb.DateTimeProperty(indexed=False, auto_now_add=True) + + +def create_new_user_confirmation(user_address): + """Create a new user confirmation. + + Args: + user_address: string, an email addres + + Returns: The url to click to confirm the email address.""" + id_chars = string.ascii_letters + string.digits + rand = random.SystemRandom() + random_id = ''.join([rand.choice(id_chars) for i in range(42)]) + record = UserConfirmationRecord(user_address=user_address, + id=random_id) + record.put() + return 'https://{}/user/confirm?code={}'.format( + socket.getfqdn(socket.gethostname()), random_id) + + +class ConfirmUserSignupHandler(webapp2.RequestHandler): + """Invoked when the user clicks on the confirmation link in the email.""" + + def get(self): + code = self.request.get('code') + if code: + record = ndb.Key(UserConfirmationRecord, code).get() + # 2-hour time limit on confirming. + if record and (datetime.datetime.now() - record.timestamp < + datetime.timedelta(hours=2)): + record.confirmed = True + record.put() + self.response.content_type = 'text/plain' + self.response.write('Confirmed {}.' + .format(record.user_address)) + return + self.response.status_int = 404 + + +app = webapp2.WSGIApplication([ + ('/user/signup', UserSignupHandler), + ('/user/confirm', ConfirmUserSignupHandler), +], debug=True) diff --git a/appengine/standard/mail/user_signup_test.py b/appengine/standard/mail/user_signup_test.py new file mode 100644 index 00000000000..c386f2ace15 --- /dev/null +++ b/appengine/standard/mail/user_signup_test.py @@ -0,0 +1,39 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import webtest + +import user_signup + + +def test_user_signup(testbed): + testbed.init_mail_stub() + testbed.init_app_identity_stub() + testbed.init_datastore_v3_stub() + app = webtest.TestApp(user_signup.app) + response = app.post('/user/signup', 'email_address=alice@example.com') + assert response.status_int == 200 + assert 'An email has been sent to alice@example.com.' in response.body + + records = user_signup.UserConfirmationRecord.query().fetch(1) + response = app.get('/user/confirm?code={}'.format(records[0].key.id())) + assert response.status_int == 200 + assert 'Confirmed alice@example.com.' in response.body + + +def test_bad_code(testbed): + testbed.init_datastore_v3_stub() + app = webtest.TestApp(user_signup.app) + response = app.get('/user/confirm?code=garbage', status=404) + assert response.status_int == 404 diff --git a/appengine/standard/mailgun/.gitignore b/appengine/standard/mailgun/.gitignore new file mode 100644 index 00000000000..a65b41774ad --- /dev/null +++ b/appengine/standard/mailgun/.gitignore @@ -0,0 +1 @@ +lib diff --git a/appengine/standard/mailgun/README.md b/appengine/standard/mailgun/README.md new file mode 100644 index 00000000000..2973827801e --- /dev/null +++ b/appengine/standard/mailgun/README.md @@ -0,0 +1,12 @@ +# Mailgun & Google App Engine + +This sample application demonstrates how to use [Mailgun with Google App Engine](https://cloud.google.com/appengine/docs/python/mail/mailgun). + +Refer to the [App Engine Samples README](../../README.md) for information on how to run and deploy this sample. + +# Setup + +Before running this sample: + +1. You will need a [Mailgun account](http://www.mailgun.com/google). +2. Update the `MAILGUN_DOMAIN_NAME` and `MAILGUN_API_KEY` constants in `main.py`. You can use your account's sandbox domain. diff --git a/appengine/standard/mailgun/app.yaml b/appengine/standard/mailgun/app.yaml new file mode 100644 index 00000000000..42ad35ed2a8 --- /dev/null +++ b/appengine/standard/mailgun/app.yaml @@ -0,0 +1,7 @@ +runtime: python27 +threadsafe: yes +api_version: 1 + +handlers: +- url: .* + script: main.app diff --git a/appengine/standard/mailgun/appengine_config.py b/appengine/standard/mailgun/appengine_config.py new file mode 100644 index 00000000000..f579c7005da --- /dev/null +++ b/appengine/standard/mailgun/appengine_config.py @@ -0,0 +1,18 @@ +# Copyright 2015 Google Inc. +# +# 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.appengine.ext import vendor + +# Add any libraries installed in the "lib" folder. +vendor.add('lib') diff --git a/appengine/standard/mailgun/main.py b/appengine/standard/mailgun/main.py new file mode 100644 index 00000000000..3ba7e7e206a --- /dev/null +++ b/appengine/standard/mailgun/main.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python + +# Copyright 2015 Google Inc. +# +# 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. + +""" +Sample Google App Engine application that demonstrates how to send mail using +Mailgun. + +For more information, see README.md. +""" + +from urllib import urlencode + +import httplib2 +import webapp2 + + +# Your Mailgun Domain Name +MAILGUN_DOMAIN_NAME = 'your-mailgun-domain-name' +# Your Mailgun API key +MAILGUN_API_KEY = 'your-mailgun-api-key' + + +# [START simple_message] +def send_simple_message(recipient): + http = httplib2.Http() + http.add_credentials('api', MAILGUN_API_KEY) + + url = 'https://api.mailgun.net/v3/{}/messages'.format(MAILGUN_DOMAIN_NAME) + data = { + 'from': 'Example Sender '.format(MAILGUN_DOMAIN_NAME), + 'to': recipient, + 'subject': 'This is an example email from Mailgun', + 'text': 'Test message from Mailgun' + } + + resp, content = http.request( + url, 'POST', urlencode(data), + headers={"Content-Type": "application/x-www-form-urlencoded"}) + + if resp.status != 200: + raise RuntimeError( + 'Mailgun API error: {} {}'.format(resp.status, content)) +# [END simple_message] + + +# [START complex_message] +def send_complex_message(recipient): + http = httplib2.Http() + http.add_credentials('api', MAILGUN_API_KEY) + + url = 'https://api.mailgun.net/v3/{}/messages'.format(MAILGUN_DOMAIN_NAME) + data = { + 'from': 'Example Sender '.format(MAILGUN_DOMAIN_NAME), + 'to': recipient, + 'subject': 'This is an example email from Mailgun', + 'text': 'Test message from Mailgun', + 'html': 'HTML version of the body' + } + + resp, content = http.request( + url, 'POST', urlencode(data), + headers={"Content-Type": "application/x-www-form-urlencoded"}) + + if resp.status != 200: + raise RuntimeError( + 'Mailgun API error: {} {}'.format(resp.status, content)) +# [END complex_message] + + +class MainPage(webapp2.RequestHandler): + def get(self): + self.response.content_type = 'text/html' + self.response.write(""" + + +
    + + + +
    + +""") + + def post(self): + recipient = self.request.get('recipient') + action = self.request.get('submit') + + if action == 'Send simple email': + send_simple_message(recipient) + else: + send_complex_message(recipient) + + self.response.write('Mail sent') + + +app = webapp2.WSGIApplication([ + ('/', MainPage) +], debug=True) diff --git a/appengine/standard/mailgun/main_test.py b/appengine/standard/mailgun/main_test.py new file mode 100644 index 00000000000..91980afe867 --- /dev/null +++ b/appengine/standard/mailgun/main_test.py @@ -0,0 +1,67 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from googleapiclient.http import HttpMockSequence +import httplib2 +import mock +import pytest +import webtest + +import main + + +class HttpMockSequenceWithCredentials(HttpMockSequence): + def add_credentials(self, *args): + pass + + +@pytest.fixture +def app(): + return webtest.TestApp(main.app) + + +def test_get(app): + response = app.get('/') + assert response.status_int == 200 + + +def test_post(app): + http = HttpMockSequenceWithCredentials([ + ({'status': '200'}, '')]) + patch_http = mock.patch.object(httplib2, 'Http', lambda: http) + + with patch_http: + response = app.post('/', { + 'recipient': 'jonwayne@google.com', + 'submit': 'Send simple email'}) + + assert response.status_int == 200 + + http = HttpMockSequenceWithCredentials([ + ({'status': '200'}, '')]) + + with patch_http: + response = app.post('/', { + 'recipient': 'jonwayne@google.com', + 'submit': 'Send complex email'}) + + assert response.status_int == 200 + + http = HttpMockSequenceWithCredentials([ + ({'status': '500'}, 'Test error')]) + + with patch_http, pytest.raises(Exception): + app.post('/', { + 'recipient': 'jonwayne@google.com', + 'submit': 'Send simple email'}) diff --git a/appengine/standard/mailgun/requirements.txt b/appengine/standard/mailgun/requirements.txt new file mode 100644 index 00000000000..9ef73199ca7 --- /dev/null +++ b/appengine/standard/mailgun/requirements.txt @@ -0,0 +1 @@ +httplib2==0.10.3 diff --git a/appengine/standard/mailjet/.gitignore b/appengine/standard/mailjet/.gitignore new file mode 100644 index 00000000000..a65b41774ad --- /dev/null +++ b/appengine/standard/mailjet/.gitignore @@ -0,0 +1 @@ +lib diff --git a/appengine/standard/mailjet/README.md b/appengine/standard/mailjet/README.md new file mode 100644 index 00000000000..3880c174d4d --- /dev/null +++ b/appengine/standard/mailjet/README.md @@ -0,0 +1,11 @@ +# Python Mailjet email sample for Google App Engine Standard + +This sample demonstrates how to use [Mailjet](https://www.mailgun.com) on [Google App Engine Standard](https://cloud.google.com/appengine/docs/). + +## Setup + +Before you can run or deploy the sample, you will need to do the following: + +1. [Create a Mailjet Account](http://www.mailjet.com/google). + +2. Configure your Mailjet settings in the environment variables section in ``app.yaml``. diff --git a/appengine/standard/mailjet/app.yaml b/appengine/standard/mailjet/app.yaml new file mode 100644 index 00000000000..467dd8e55be --- /dev/null +++ b/appengine/standard/mailjet/app.yaml @@ -0,0 +1,14 @@ +runtime: python27 +threadsafe: yes +api_version: 1 + +handlers: +- url: .* + script: main.app + +# [START env_variables] +env_variables: + MAILJET_API_KEY: your-mailjet-api-key + MAILJET_API_SECRET: your-mailjet-api-secret + MAILJET_SENDER: your-mailjet-sender-address +# [END env_variables] diff --git a/appengine/standard/mailjet/appengine_config.py b/appengine/standard/mailjet/appengine_config.py new file mode 100644 index 00000000000..c903d9a0ac5 --- /dev/null +++ b/appengine/standard/mailjet/appengine_config.py @@ -0,0 +1,18 @@ +# Copyright 2016 Google Inc. +# +# 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.appengine.ext import vendor + +# Add any libraries installed in the "lib" folder. +vendor.add('lib') diff --git a/appengine/standard/mailjet/main.py b/appengine/standard/mailjet/main.py new file mode 100644 index 00000000000..122a1edb09d --- /dev/null +++ b/appengine/standard/mailjet/main.py @@ -0,0 +1,77 @@ +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START app] +import logging +import os + +from flask import Flask, render_template, request +# [START config] +import mailjet_rest +import requests_toolbelt.adapters.appengine + +# Use the App Engine requests adapter to allow the requests library to be +# used on App Engine. +requests_toolbelt.adapters.appengine.monkeypatch() + +MAILJET_API_KEY = os.environ['MAILJET_API_KEY'] +MAILJET_API_SECRET = os.environ['MAILJET_API_SECRET'] +MAILJET_SENDER = os.environ['MAILJET_SENDER'] +# [END config] + +app = Flask(__name__) + + +# [START send_message] +def send_message(to): + client = mailjet_rest.Client( + auth=(MAILJET_API_KEY, MAILJET_API_SECRET)) + + data = { + 'FromEmail': MAILJET_SENDER, + 'FromName': 'App Engine Flex Mailjet Sample', + 'Subject': 'Example email.', + 'Text-part': 'This is an example email.', + 'Html-part': 'This is an example email.', + 'Recipients': [{'Email': to}] + } + + result = client.send.create(data=data) + + return result.json() +# [END send_message] + + +@app.route('/') +def index(): + return render_template('index.html') + + +@app.route('/send/email', methods=['POST']) +def send_email(): + to = request.form.get('to') + + result = send_message(to) + + return 'Email sent, response:
    {}
    '.format(result) + + +@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 +# [END app] diff --git a/appengine/standard/mailjet/main_test.py b/appengine/standard/mailjet/main_test.py new file mode 100644 index 00000000000..c910f48c544 --- /dev/null +++ b/appengine/standard/mailjet/main_test.py @@ -0,0 +1,53 @@ +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import re + +import pytest +import responses + + +@pytest.fixture +def app(monkeypatch): + monkeypatch.setenv('MAILJET_API_KEY', 'apikey') + monkeypatch.setenv('MAILJET_API_SECRET', 'apisecret') + monkeypatch.setenv('MAILJET_SENDER', 'sender') + + import main + + main.app.testing = True + return main.app.test_client() + + +def test_index(app): + r = app.get('/') + assert r.status_code == 200 + + +@responses.activate +def test_send_email(app): + responses.add( + responses.POST, + re.compile(r'.*'), + body='{"test": "message"}', + content_type='application/json') + + r = app.post('/send/email', data={'to': 'user@example.com'}) + + assert r.status_code == 200 + assert 'test' in r.data.decode('utf-8') + + assert len(responses.calls) == 1 + request_body = responses.calls[0].request.body + assert 'user@example.com' in request_body diff --git a/appengine/standard/mailjet/requirements.txt b/appengine/standard/mailjet/requirements.txt new file mode 100644 index 00000000000..35b53b8b9ce --- /dev/null +++ b/appengine/standard/mailjet/requirements.txt @@ -0,0 +1,4 @@ +Flask==0.12.2 +requests==2.18.1 +requests-toolbelt==0.8.0 +mailjet-rest==1.3.0 diff --git a/appengine/standard/mailjet/templates/index.html b/appengine/standard/mailjet/templates/index.html new file mode 100644 index 00000000000..cd1c93ff5b3 --- /dev/null +++ b/appengine/standard/mailjet/templates/index.html @@ -0,0 +1,29 @@ +{# +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#} + + + + Mailjet on Google App Engine + + + +
    + + +
    + + + diff --git a/appengine/standard/memcache/best_practices/README.md b/appengine/standard/memcache/best_practices/README.md new file mode 100644 index 00000000000..9c72cb15aa8 --- /dev/null +++ b/appengine/standard/memcache/best_practices/README.md @@ -0,0 +1,5 @@ +# Memcache Best Practices + +Code snippets for [Memcache Cache Best Practices article](https://cloud.google.com/appengine/articles/best-practices-for-app-engine-memcache) + + diff --git a/appengine/standard/memcache/best_practices/batch/app.yaml b/appengine/standard/memcache/best_practices/batch/app.yaml new file mode 100644 index 00000000000..ef0ebcae28b --- /dev/null +++ b/appengine/standard/memcache/best_practices/batch/app.yaml @@ -0,0 +1,7 @@ +runtime: python27 +threadsafe: yes +api_version: 1 + +handlers: +- url: .* + script: batch.app diff --git a/appengine/standard/memcache/best_practices/batch/batch.py b/appengine/standard/memcache/best_practices/batch/batch.py new file mode 100644 index 00000000000..2f0119c579c --- /dev/null +++ b/appengine/standard/memcache/best_practices/batch/batch.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python + +# Copyright 2016 Google Inc. +# +# 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 google.appengine.api import memcache +import webapp2 + + +class MainPage(webapp2.RequestHandler): + def get(self): + # [START batch] + values = {'comment': 'I did not ... ', 'comment_by': 'Bill Holiday'} + if not memcache.set_multi(values): + logging.error('Unable to set Memcache values') + tvalues = memcache.get_multi(('comment', 'comment_by')) + self.response.write(tvalues) + # [END batch] + + +app = webapp2.WSGIApplication([ + ('/', MainPage), +], debug=True) diff --git a/appengine/standard/memcache/best_practices/batch/batch_test.py b/appengine/standard/memcache/best_practices/batch/batch_test.py new file mode 100644 index 00000000000..ca0883f362e --- /dev/null +++ b/appengine/standard/memcache/best_practices/batch/batch_test.py @@ -0,0 +1,28 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest +import webtest + +import batch + + +@pytest.fixture +def app(testbed): + return webtest.TestApp(batch.app) + + +def test_get(app): + response = app.get('/') + assert 'Bill Holiday' in response.body diff --git a/appengine/standard/memcache/best_practices/failure/app.yaml b/appengine/standard/memcache/best_practices/failure/app.yaml new file mode 100644 index 00000000000..60fabb0490e --- /dev/null +++ b/appengine/standard/memcache/best_practices/failure/app.yaml @@ -0,0 +1,7 @@ +runtime: python27 +threadsafe: yes +api_version: 1 + +handlers: +- url: .* + script: failure.app diff --git a/appengine/standard/memcache/best_practices/failure/failure.py b/appengine/standard/memcache/best_practices/failure/failure.py new file mode 100644 index 00000000000..767d2094a78 --- /dev/null +++ b/appengine/standard/memcache/best_practices/failure/failure.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python + +# Copyright 2016 Google Inc. +# +# 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 google.appengine.api import memcache +import webapp2 + + +def read_from_persistent_store(): + """Fake method for demonstration purposes. Usually would return + a value from a database like Cloud Datastore or MySQL.""" + return "a persistent value" + + +class ReadPage(webapp2.RequestHandler): + def get(self): + key = "some-key" + # [START memcache-read] + v = memcache.get(key) + if v is None: + v = read_from_persistent_store() + memcache.add(key, v) + # [END memcache-read] + + self.response.content_type = 'text/html' + self.response.write(str(v)) + + +class DeletePage(webapp2.RequestHandler): + def get(self): + key = "some key" + seconds = 5 + memcache.set(key, "some value") + # [START memcache-delete] + memcache.delete(key, seconds) # clears cache + # write to persistent datastore + # Do not attempt to put new value in cache, first reader will do that + # [END memcache-delete] + self.response.content_type = 'text/html' + self.response.write('done') + + +class MainPage(webapp2.RequestHandler): + def get(self): + value = 3 + # [START memcache-failure] + if not memcache.set('counter', value): + logging.error("Memcache set failed") + # Other error handling here + # [END memcache-failure] + self.response.content_type = 'text/html' + self.response.write('done') + + +app = webapp2.WSGIApplication([ + ('/', MainPage), + ('/delete', DeletePage), + ('/read', ReadPage), +], debug=True) diff --git a/appengine/standard/memcache/best_practices/failure/failure_test.py b/appengine/standard/memcache/best_practices/failure/failure_test.py new file mode 100644 index 00000000000..125054be64c --- /dev/null +++ b/appengine/standard/memcache/best_practices/failure/failure_test.py @@ -0,0 +1,35 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest +import webtest + +import failure + + +@pytest.fixture +def app(testbed): + return webtest.TestApp(failure.app) + + +def test_get(app): + app.get('/') + + +def test_read(app): + app.get('/read') + + +def test_delete(app): + app.get('/delete') diff --git a/appengine/standard/memcache/best_practices/migration_step1/app.yaml b/appengine/standard/memcache/best_practices/migration_step1/app.yaml new file mode 100644 index 00000000000..02e9132c4b0 --- /dev/null +++ b/appengine/standard/memcache/best_practices/migration_step1/app.yaml @@ -0,0 +1,7 @@ +runtime: python27 +threadsafe: yes +api_version: 1 + +handlers: +- url: .* + script: migration1.app diff --git a/appengine/standard/memcache/best_practices/migration_step1/migration1.py b/appengine/standard/memcache/best_practices/migration_step1/migration1.py new file mode 100644 index 00000000000..3cd3ad7a725 --- /dev/null +++ b/appengine/standard/memcache/best_practices/migration_step1/migration1.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python + +# Copyright 2016 Google Inc. +# +# 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 google.appengine.api import memcache +from google.appengine.ext import ndb +import webapp2 + + +# [START best-practice-1] +class Person(ndb.Model): + name = ndb.StringProperty(required=True) + + +def get_or_add_person(name): + person = memcache.get(name) + if person is None: + person = Person(name=name) + memcache.add(name, person) + else: + logging.info('Found in cache: ' + name) + return person +# [END best-practice-1] + + +class MainPage(webapp2.RequestHandler): + def get(self): + person = get_or_add_person('Stevie Wonder') + self.response.content_type = 'text/html' + self.response.write(person.name) + + +app = webapp2.WSGIApplication([ + ('/', MainPage), +], debug=True) diff --git a/appengine/standard/memcache/best_practices/migration_step1/migration1_test.py b/appengine/standard/memcache/best_practices/migration_step1/migration1_test.py new file mode 100644 index 00000000000..fe1b4b7acc8 --- /dev/null +++ b/appengine/standard/memcache/best_practices/migration_step1/migration1_test.py @@ -0,0 +1,22 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import webtest + +import migration1 + + +def test_get(testbed): + app = webtest.TestApp(migration1.app) + app.get('/') diff --git a/appengine/standard/memcache/best_practices/migration_step2/app.yaml b/appengine/standard/memcache/best_practices/migration_step2/app.yaml new file mode 100644 index 00000000000..0a2dd7051d8 --- /dev/null +++ b/appengine/standard/memcache/best_practices/migration_step2/app.yaml @@ -0,0 +1,7 @@ +runtime: python27 +threadsafe: yes +api_version: 1 + +handlers: +- url: .* + script: migration2.app diff --git a/appengine/standard/memcache/best_practices/migration_step2/migration2.py b/appengine/standard/memcache/best_practices/migration_step2/migration2.py new file mode 100644 index 00000000000..1f3b0c810ee --- /dev/null +++ b/appengine/standard/memcache/best_practices/migration_step2/migration2.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python + +# Copyright 2016 Google Inc. +# +# 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 google.appengine.api import memcache +from google.appengine.ext import ndb +import webapp2 + + +# [START best-practice-2] +class Person(ndb.Model): + name = ndb.StringProperty(required=True) + userid = ndb.StringProperty(required=True) + + +def get_or_add_person(name, userid): + person = memcache.get(name) + if person is None: + person = Person(name=name, userid=userid) + memcache.add(name, person) + else: + logging.info('Found in cache: ' + name + ', userid: ' + person.userid) + return person +# [END best-practice-2] + + +class MainPage(webapp2.RequestHandler): + def get(self): + person = get_or_add_person('Stevie Wonder', "1") + self.response.content_type = 'text/html' + self.response.write(person.name) + + +app = webapp2.WSGIApplication([ + ('/', MainPage), +], debug=True) diff --git a/appengine/standard/memcache/best_practices/migration_step2/migration2_test.py b/appengine/standard/memcache/best_practices/migration_step2/migration2_test.py new file mode 100644 index 00000000000..b2441c4331f --- /dev/null +++ b/appengine/standard/memcache/best_practices/migration_step2/migration2_test.py @@ -0,0 +1,22 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import webtest + +import migration2 + + +def test_get(testbed): + app = webtest.TestApp(migration2.app) + app.get('/') diff --git a/appengine/standard/memcache/best_practices/sharing/app.yaml b/appengine/standard/memcache/best_practices/sharing/app.yaml new file mode 100644 index 00000000000..17dc52934b5 --- /dev/null +++ b/appengine/standard/memcache/best_practices/sharing/app.yaml @@ -0,0 +1,7 @@ +runtime: python27 +threadsafe: yes +api_version: 1 + +handlers: +- url: .* + script: sharing.app diff --git a/appengine/standard/memcache/best_practices/sharing/sharing.py b/appengine/standard/memcache/best_practices/sharing/sharing.py new file mode 100644 index 00000000000..3371c0ef1eb --- /dev/null +++ b/appengine/standard/memcache/best_practices/sharing/sharing.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python + +# Copyright 2016 Google Inc. +# +# 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.appengine.api import memcache +import webapp2 + + +class MainPage(webapp2.RequestHandler): + def get(self): + # [START sharing] + self.response.headers['Content-Type'] = 'text/plain' + + who = memcache.get('who') + self.response.write('Previously incremented by %s\n' % who) + memcache.set('who', 'Python') + + count = memcache.incr('count', 1, initial_value=0) + self.response.write('Count incremented by Python = %s\n' % count) + # [END sharing] + + +app = webapp2.WSGIApplication([ + ('/', MainPage), +], debug=True) diff --git a/appengine/standard/memcache/best_practices/sharing/sharing_test.py b/appengine/standard/memcache/best_practices/sharing/sharing_test.py new file mode 100644 index 00000000000..f1cb8d0a1d6 --- /dev/null +++ b/appengine/standard/memcache/best_practices/sharing/sharing_test.py @@ -0,0 +1,23 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import webtest + +import sharing + + +def test_get(testbed): + app = webtest.TestApp(sharing.app) + response = app.get('/') + assert 'Previously incremented by ' in response.body diff --git a/appengine/standard/memcache/guestbook/README.md b/appengine/standard/memcache/guestbook/README.md new file mode 100644 index 00000000000..55496d13b23 --- /dev/null +++ b/appengine/standard/memcache/guestbook/README.md @@ -0,0 +1,13 @@ +# Memcache Guestbook Sample + +This is a sample app for Google App Engine that demonstrates the Memcache Python API. + + +These samples are used on the following documentation page: + +> https://cloud.google.com/appengine/docs/python/memcache/examples + + + +Refer to the [App Engine Samples README](../../README.md) for information on how to run and deploy this sample. +ple. diff --git a/appengine/standard/memcache/guestbook/app.yaml b/appengine/standard/memcache/guestbook/app.yaml new file mode 100644 index 00000000000..1676c4ac14c --- /dev/null +++ b/appengine/standard/memcache/guestbook/app.yaml @@ -0,0 +1,21 @@ +# This file specifies your Python application's runtime configuration +# including URL routing, versions, static file uploads, etc. See +# https://developers.google.com/appengine/docs/python/config/appconfig +# for details. + +runtime: python27 +api_version: 1 +threadsafe: yes + +# Handlers define how to route requests to your application. +handlers: + +# This handler tells app engine how to route requests to a WSGI application. +# The script value is in the format . +# where is a WSGI application object. +- url: .* # This regex directs all routes to main.app + script: main.app + +libraries: +- name: webapp2 + version: "2.5.2" diff --git a/appengine/standard/memcache/guestbook/favicon.ico b/appengine/standard/memcache/guestbook/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..23c553a2966ca4cecf146093f33d8114b4f1e368 GIT binary patch literal 8348 zcmeI0du&tJ8Ng3Vwv|ewX4Hu@my&|vCD%DN@CLnxyptBT_%HjatoYr7%N3zGPed#@ckvA><;HWecDU4&2}*2(h%gm&WgCftWj zkQfW;&mXr@0S&(csd+cjaImbXSy=NC<10}z(efGwgi=;B$dt=HkKYCvp-#3KX+qIu zxx$>zcwk+N=c%1^J9)sO44lmS0##~wzU(43f>wW-p_YXwj>s>3{gHI*^oo1lmJ!X*bw{3UBAKRut zUh5Uy4_94IIkdxC^v{_g%FjuEI+f&;9KQ3i+nK7pU_RivFjP_DTl-&gP_qr$nD=M{ z@z;C3#y5Jsh77svGQ4u$ZX)t}=e52{#Xvk;4qIxNh86kR$OhCAie%&e`U{r{ACQZ@ z-)1#sItef{2L za&U;nm+_lo@lbR7vat&^EcECsH)uSnhlht@etsV4LE5B`j#GW*iur~}xpYlTtc;FC{nl|LK^miZ0y3WU>MIM zmc}94VzEFx9?#&?4l+h;ggzvOeCI$ob=`tBp*&EtirH2xU z4a#KB)IMa{_YdYrnxq>-DrrQ>rfpYe8@|Nc-oHnG*L(HR$}d4Eo2&X@o1~9l@%@W) zU{%rv$`tBAOHJH+?zcv7`!T~x;=<#7@4R5T^5$-%Pz-NAYt+5{d;>R+9W24y&`;ZkZqFGQ6_)<35Z_$5V#ga#=N9985-M0KR*fl@h4M0BxWvbYQr6t zULtYBAMJ%iU>x|?U8z_Zyv1jg_VcZ^kO)pd_)jk`_~2MHZmybbloW?l)lnMrb~TAX zV&%#e+VLvM4!%js+%B6}N!=udFlN5Jv;yQm0sdW({6ny+{{$_b`%pI&q3%#p3R?q( z%3@zpafPo4-GA}ErIfU@j?gpfdgeyI);;S-otz(GefQbXvG3J6hbwD_0*YOuqb1V8 zpQm{(oT|g$!tcw;Ck6l>=mo$z0J@0f0^U0{x?+mD8}QB{9yV7qQ;&$5^%*fb@qUB& zX(O<{D-aZ493K;1xH%!}9+}vt8Sshs4os9)d132glTa=lIJv}MJyU_Y!ZFl62j9@l zggg2y{y~c&Vm0cMa@}r@bbn?HR4Sa|66p;nlMl_6D%_%EjCNRq)NBvx!R!t`@zUoW zKV#O#EhZy4>~?VU+rfg@_W_4Kt~zS<|3JjVMcZ#exy;pDUypq|xjpD&0#H{BHgrvM zI=z9nnTN~NCX?mf@DOU;!82-#gXGsw5CSSf22lJ>u8duE&igGuZq4lVZf*LH2%QqOqkv@O{w`Y}q~yWm0EP zeSP~Hau49ZBU@&hWwH5YG0dnGVN1^iztQFh>rIvj5$iQ;q^nyuSuXo`U~{E8DpuIS zA{kR5oC9o=_>jhfRX@QTs1KRm{@F31 zFKP1!w?51@R^I~kA%H*B0W^sKnyVIsv`_2;=zbUGR9pOTqUhV{{^UH=RQ2@S?`uaQ zEy`)Gsd}1IEedW&4lAe0Sg5h`nQXqaZ}RyEzX{FT?hmF3>0_QQT1V~jI$wc&1@aZh ZS0G=3d706zJ{{SaIO|Jj| literal 0 HcmV?d00001 diff --git a/appengine/standard/memcache/guestbook/index.yaml b/appengine/standard/memcache/guestbook/index.yaml new file mode 100644 index 00000000000..f933a1c8e6d --- /dev/null +++ b/appengine/standard/memcache/guestbook/index.yaml @@ -0,0 +1,17 @@ +indexes: + +# AUTOGENERATED + +# This index.yaml is automatically updated whenever the dev_appserver +# detects that a new type of query is run. If you want to manage the +# index.yaml file manually, remove the above marker line (the line +# saying "# AUTOGENERATED"). If you want to manage some indexes +# manually, move them above the marker line. The index.yaml file is +# automatically uploaded to the admin console when you next deploy +# your application using appcfg.py. + +- kind: Greeting + ancestor: yes + properties: + - name: date + direction: desc diff --git a/appengine/standard/memcache/guestbook/main.py b/appengine/standard/memcache/guestbook/main.py new file mode 100644 index 00000000000..ad087b2ac72 --- /dev/null +++ b/appengine/standard/memcache/guestbook/main.py @@ -0,0 +1,151 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Sample application that demonstrates how to use the App Engine Memcache API. + +For more information, see README.md. +""" + +# [START all] + +import cgi +import cStringIO +import logging +import urllib + +from google.appengine.api import memcache +from google.appengine.api import users +from google.appengine.ext import ndb + +import webapp2 + + +class Greeting(ndb.Model): + """Models an individual Guestbook entry with author, content, and date.""" + author = ndb.StringProperty() + content = ndb.StringProperty() + date = ndb.DateTimeProperty(auto_now_add=True) + + +def guestbook_key(guestbook_name=None): + """Constructs a Datastore key for a Guestbook entity with guestbook_name""" + return ndb.Key('Guestbook', guestbook_name or 'default_guestbook') + + +class MainPage(webapp2.RequestHandler): + def get(self): + self.response.out.write('') + guestbook_name = self.request.get('guestbook_name') + + greetings = self.get_greetings(guestbook_name) + stats = memcache.get_stats() + + self.response.write('Cache Hits:{}
    '.format(stats['hits'])) + self.response.write('Cache Misses:{}

    '.format( + stats['misses'])) + self.response.write(greetings) + + self.response.write(""" +
    +
    +
    +
    +
    +
    Guestbook name: +
    + + """.format(urllib.urlencode({'guestbook_name': guestbook_name}), + cgi.escape(guestbook_name))) + + # [START check_memcache] + def get_greetings(self, guestbook_name): + """ + get_greetings() + Checks the cache to see if there are cached greetings. + If not, call render_greetings and set the cache + + Args: + guestbook_name: Guestbook entity group key (string). + + Returns: + A string of HTML containing greetings. + """ + greetings = memcache.get('{}:greetings'.format(guestbook_name)) + if greetings is None: + greetings = self.render_greetings(guestbook_name) + try: + added = memcache.add( + '{}:greetings'.format(guestbook_name), greetings, 10) + if not added: + logging.error('Memcache set failed.') + except ValueError: + logging.error('Memcache set failed - data larger than 1MB') + return greetings + # [END check_memcache] + + # [START query_datastore] + def render_greetings(self, guestbook_name): + """ + render_greetings() + Queries the database for greetings, iterate through the + results and create the HTML. + + Args: + guestbook_name: Guestbook entity group key (string). + + Returns: + A string of HTML containing greetings + """ + greetings = ndb.gql('SELECT * ' + 'FROM Greeting ' + 'WHERE ANCESTOR IS :1 ' + 'ORDER BY date DESC LIMIT 10', + guestbook_key(guestbook_name)) + output = cStringIO.StringIO() + for greeting in greetings: + if greeting.author: + output.write('{} wrote:'.format(greeting.author)) + else: + output.write('An anonymous person wrote:') + output.write('
    {}
    '.format( + cgi.escape(greeting.content))) + return output.getvalue() + # [END query_datastore] + + +class Guestbook(webapp2.RequestHandler): + def post(self): + # We set the same parent key on the 'Greeting' to ensure each greeting + # is in the same entity group. Queries across the single entity group + # are strongly consistent. However, the write rate to a single entity + # group is limited to ~1/second. + guestbook_name = self.request.get('guestbook_name') + greeting = Greeting(parent=guestbook_key(guestbook_name)) + + if users.get_current_user(): + greeting.author = users.get_current_user().nickname() + + greeting.content = self.request.get('content') + greeting.put() + memcache.delete('{}:greetings'.format(guestbook_name)) + self.redirect('/?' + + urllib.urlencode({'guestbook_name': guestbook_name})) + + +app = webapp2.WSGIApplication([('/', MainPage), + ('/sign', Guestbook)], + debug=True) + +# [END all] diff --git a/appengine/standard/memcache/guestbook/main_test.py b/appengine/standard/memcache/guestbook/main_test.py new file mode 100644 index 00000000000..1851b78db88 --- /dev/null +++ b/appengine/standard/memcache/guestbook/main_test.py @@ -0,0 +1,23 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import webtest + +import main + + +def test_app(testbed): + app = webtest.TestApp(main.app) + response = app.get('/') + assert response.status_int == 200 diff --git a/appengine/standard/memcache/snippets/snippets.py b/appengine/standard/memcache/snippets/snippets.py new file mode 100644 index 00000000000..a4ce4867f51 --- /dev/null +++ b/appengine/standard/memcache/snippets/snippets.py @@ -0,0 +1,54 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START import] +from google.appengine.api import memcache +# [END import] + + +def query_for_data(): + return 'data' + + +# [START get_data] +def get_data(): + data = memcache.get('key') + if data is not None: + return data + else: + data = query_for_data() + memcache.add('key', data, 60) + return data +# [END get_data] + + +def add_values(): + # [START add_values] + # Add a value if it doesn't exist in the cache + # with a cache expiration of 1 hour. + memcache.add(key="weather_USA_98105", value="raining", time=3600) + + # Set several values, overwriting any existing values for these keys. + memcache.set_multi( + {"USA_98115": "cloudy", "USA_94105": "foggy", "USA_94043": "sunny"}, + key_prefix="weather_", + time=3600 + ) + + # Atomically increment an integer value. + memcache.set(key="counter", value=0) + memcache.incr("counter") + memcache.incr("counter") + memcache.incr("counter") + # [END add_values] diff --git a/appengine/standard/memcache/snippets/snippets_test.py b/appengine/standard/memcache/snippets/snippets_test.py new file mode 100644 index 00000000000..8d34d8f408b --- /dev/null +++ b/appengine/standard/memcache/snippets/snippets_test.py @@ -0,0 +1,50 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from google.appengine.api import memcache +from mock import patch + +import snippets + + +SNIPPET_VALUES = { + "weather_USA_98105": "raining", + "weather_USA_98115": "cloudy", + "weather_USA_94105": "foggy", + "weather_USA_94043": "sunny", + "counter": 3, +} + + +@patch('snippets.query_for_data', return_value='data') +def test_get_data_not_present(query_fn, testbed): + data = snippets.get_data() + query_fn.assert_called_once_with() + assert data == 'data' + memcache.delete('key') + + +@patch('snippets.query_for_data', return_value='data') +def test_get_data_present(query_fn, testbed): + memcache.add('key', 'data', 9000) + data = snippets.get_data() + query_fn.assert_not_called() + assert data == 'data' + memcache.delete('key') + + +def test_add_values(testbed): + snippets.add_values() + for key, value in SNIPPET_VALUES.iteritems(): + assert memcache.get(key) == value diff --git a/appengine/standard/modules/README.md b/appengine/standard/modules/README.md new file mode 100644 index 00000000000..82bb51d55ff --- /dev/null +++ b/appengine/standard/modules/README.md @@ -0,0 +1,5 @@ +## App Engine Modules Docs Snippets + +This sample application demonstrates how to use Google App Engine's modules API. + + diff --git a/appengine/standard/modules/app.yaml b/appengine/standard/modules/app.yaml new file mode 100644 index 00000000000..102ed60d1b5 --- /dev/null +++ b/appengine/standard/modules/app.yaml @@ -0,0 +1,7 @@ +runtime: python27 +api_version: 1 +threadsafe: yes + +handlers: +- url: .* + script: main.app diff --git a/appengine/standard/modules/backend.py b/appengine/standard/modules/backend.py new file mode 100644 index 00000000000..efe8318deff --- /dev/null +++ b/appengine/standard/modules/backend.py @@ -0,0 +1,29 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Sample backend module deployed by backend.yaml and accessed in main.py +""" + +import webapp2 + + +class BackendHandler(webapp2.RequestHandler): + def get(self): + self.response.write("hello world") + + +app = webapp2.WSGIApplication([ + ('/', BackendHandler), +], debug=True) diff --git a/appengine/standard/modules/backend.yaml b/appengine/standard/modules/backend.yaml new file mode 100644 index 00000000000..3beb1ed881f --- /dev/null +++ b/appengine/standard/modules/backend.yaml @@ -0,0 +1,8 @@ +runtime: python27 +api_version: 1 +threadsafe: yes +module: my-backend + +handlers: +- url: .* + script: backend.app diff --git a/appengine/standard/modules/backend_test.py b/appengine/standard/modules/backend_test.py new file mode 100644 index 00000000000..e4022c95586 --- /dev/null +++ b/appengine/standard/modules/backend_test.py @@ -0,0 +1,29 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest +import webtest + +import backend + + +@pytest.fixture +def app(): + return webtest.TestApp(backend.app) + + +def test_get_module_info(app): + result = app.get('/') + assert result.status_code == 200 + assert 'hello world' in result.body diff --git a/appengine/standard/modules/main.py b/appengine/standard/modules/main.py new file mode 100644 index 00000000000..202daa39346 --- /dev/null +++ b/appengine/standard/modules/main.py @@ -0,0 +1,53 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Sample application that demonstrates getting information about this +AppEngine modules and accessing other modules in the same project. +""" + +import urllib2 +# [START modules_import] +from google.appengine.api import modules +# [END modules_import] +import webapp2 + + +class GetModuleInfoHandler(webapp2.RequestHandler): + def get(self): + # [START module_info] + module = modules.get_current_module_name() + instance_id = modules.get_current_instance_id() + self.response.write( + 'module_id={}&instance_id={}'.format(module, instance_id)) + # [END module_info] + + +class GetBackendHandler(webapp2.RequestHandler): + def get(self): + # [START access_another_module] + backend_hostname = modules.get_hostname(module='my-backend') + url = "http://{}/".format(backend_hostname) + try: + result = urllib2.urlopen(url).read() + self.response.write('Got response {}'.format(result)) + except urllib2.URLError: + pass + # [END access_another_module] + + +app = webapp2.WSGIApplication([ + ('/', GetModuleInfoHandler), + ('/access_backend', GetBackendHandler), +], debug=True) diff --git a/appengine/standard/modules/main_test.py b/appengine/standard/modules/main_test.py new file mode 100644 index 00000000000..6ea602d425b --- /dev/null +++ b/appengine/standard/modules/main_test.py @@ -0,0 +1,46 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import mock +import pytest +import webtest + +import main + + +@pytest.fixture +def app(): + return webtest.TestApp(main.app) + + +@mock.patch("main.modules") +def test_get_module_info(modules_mock, app): + modules_mock.get_current_module_name.return_value = "default" + modules_mock.get_current_instance_id.return_value = 1 + response = app.get('/') + assert response.status_int == 200 + results = response.body.split('&') + assert results[0].split('=')[1] == 'default' + assert results[1].split('=')[1] == '1' + + +@mock.patch("main.modules") +@mock.patch("urllib2.urlopen") +def test_get_backend(url_open_mock, modules_mock, app): + url_read_mock = mock.Mock(read=mock.Mock(return_value='hello world')) + url_open_mock.return_value = url_read_mock + response = app.get('/access_backend') + + assert response.status_int == 200 + assert response.body == 'Got response hello world' diff --git a/appengine/standard/multitenancy/README.md b/appengine/standard/multitenancy/README.md new file mode 100644 index 00000000000..2b1f4a1688e --- /dev/null +++ b/appengine/standard/multitenancy/README.md @@ -0,0 +1,12 @@ +# Google App Engine Namespaces + +This sample demonstrates how to use Google App Engine's [Namespace Manager API](https://cloud.google.com/appengine/docs/python/multitenancy/multitenancy). + + +These samples are used on the following documentation page: + +> https://cloud.google.com/appengine/docs/python/multitenancy/multitenancy + + + +Refer to the [App Engine Samples README](../README.md) for information on how to run and deploy this sample. diff --git a/appengine/standard/multitenancy/app.yaml b/appengine/standard/multitenancy/app.yaml new file mode 100644 index 00000000000..47261e34268 --- /dev/null +++ b/appengine/standard/multitenancy/app.yaml @@ -0,0 +1,13 @@ +version: 1 +runtime: python27 +api_version: 1 +threadsafe: yes + +handlers: +- url: /datastore.* + script: datastore.app +- url: /memcache.* + script: memcache.app +- url: /task.* + script: taskqueue.app + diff --git a/appengine/standard/multitenancy/datastore.py b/appengine/standard/multitenancy/datastore.py new file mode 100644 index 00000000000..f0db2beccc0 --- /dev/null +++ b/appengine/standard/multitenancy/datastore.py @@ -0,0 +1,70 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Sample App Engine application demonstrating how to use the Namespace Manager +API with Datastore. + +For more information, see README.md. +""" + +# [START all] +from google.appengine.api import namespace_manager +from google.appengine.ext import ndb +import webapp2 + + +class Counter(ndb.Model): + count = ndb.IntegerProperty() + + +@ndb.transactional +def update_counter(name): + """Increment the named counter by 1.""" + counter = Counter.get_by_id(name) + if counter is None: + counter = Counter(id=name, count=0) + + counter.count += 1 + counter.put() + + return counter.count + + +class DatastoreCounterHandler(webapp2.RequestHandler): + """Increments counters in the global namespace as well as in whichever + namespace is specified by the request, which is arbitrarily named 'default' + if not specified.""" + + def get(self, namespace='default'): + global_count = update_counter('counter') + + # Save the current namespace. + previous_namespace = namespace_manager.get_namespace() + try: + namespace_manager.set_namespace(namespace) + namespace_count = update_counter('counter') + finally: + # Restore the saved namespace. + namespace_manager.set_namespace(previous_namespace) + + self.response.write('Global: {}, Namespace {}: {}'.format( + global_count, namespace, namespace_count)) + + +app = webapp2.WSGIApplication([ + (r'/datastore', DatastoreCounterHandler), + (r'/datastore/(.*)', DatastoreCounterHandler) +], debug=True) +# [END all] diff --git a/appengine/standard/multitenancy/datastore_test.py b/appengine/standard/multitenancy/datastore_test.py new file mode 100644 index 00000000000..ff23e9b301f --- /dev/null +++ b/appengine/standard/multitenancy/datastore_test.py @@ -0,0 +1,35 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import webtest + +import datastore + + +def test_datastore(testbed): + app = webtest.TestApp(datastore.app) + + response = app.get('/datastore') + assert response.status_int == 200 + assert 'Global: 1' in response.body + + response = app.get('/datastore/a') + assert response.status_int == 200 + assert 'Global: 2' in response.body + assert 'a: 1' in response.body + + response = app.get('/datastore/b') + assert response.status_int == 200 + assert 'Global: 3' in response.body + assert 'b: 1' in response.body diff --git a/appengine/standard/multitenancy/memcache.py b/appengine/standard/multitenancy/memcache.py new file mode 100644 index 00000000000..cf036d03f9b --- /dev/null +++ b/appengine/standard/multitenancy/memcache.py @@ -0,0 +1,53 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Sample App Engine application demonstrating how to use the Namespace Manager +API with Memcache. + +For more information, see README.md. +""" + +# [START all] +from google.appengine.api import memcache +from google.appengine.api import namespace_manager +import webapp2 + + +class MemcacheCounterHandler(webapp2.RequestHandler): + """Increments counters in the global namespace as well as in whichever + namespace is specified by the request, which is arbitrarily named 'default' + if not specified.""" + + def get(self, namespace='default'): + global_count = memcache.incr('counter', initial_value=0) + + # Save the current namespace. + previous_namespace = namespace_manager.get_namespace() + try: + namespace_manager.set_namespace(namespace) + namespace_count = memcache.incr('counter', initial_value=0) + finally: + # Restore the saved namespace. + namespace_manager.set_namespace(previous_namespace) + + self.response.write('Global: {}, Namespace {}: {}'.format( + global_count, namespace, namespace_count)) + + +app = webapp2.WSGIApplication([ + (r'/memcache', MemcacheCounterHandler), + (r'/memcache/(.*)', MemcacheCounterHandler) +], debug=True) +# [END all] diff --git a/appengine/standard/multitenancy/memcache_test.py b/appengine/standard/multitenancy/memcache_test.py new file mode 100644 index 00000000000..27410ad2b4d --- /dev/null +++ b/appengine/standard/multitenancy/memcache_test.py @@ -0,0 +1,35 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import webtest + +import memcache + + +def test_memcache(testbed): + app = webtest.TestApp(memcache.app) + + response = app.get('/memcache') + assert response.status_int == 200 + assert 'Global: 1' in response.body + + response = app.get('/memcache/a') + assert response.status_int == 200 + assert 'Global: 2' in response.body + assert 'a: 1' in response.body + + response = app.get('/memcache/b') + assert response.status_int == 200 + assert 'Global: 3' in response.body + assert 'b: 1' in response.body diff --git a/appengine/standard/multitenancy/taskqueue.py b/appengine/standard/multitenancy/taskqueue.py new file mode 100644 index 00000000000..64ecca9bff8 --- /dev/null +++ b/appengine/standard/multitenancy/taskqueue.py @@ -0,0 +1,91 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Sample App Engine application demonstrating how to use the Namespace Manager +API with Memcache. + +For more information, see README.md. +""" + +# [START all] +from google.appengine.api import namespace_manager +from google.appengine.api import taskqueue +from google.appengine.ext import ndb +import webapp2 + + +class Counter(ndb.Model): + count = ndb.IntegerProperty() + + +@ndb.transactional +def update_counter(name): + """Increment the named counter by 1.""" + counter = Counter.get_by_id(name) + if counter is None: + counter = Counter(id=name, count=0) + + counter.count += 1 + counter.put() + + return counter.count + + +def get_count(name): + counter = Counter.get_by_id(name) + if not counter: + return 0 + return counter.count + + +class DeferredCounterHandler(webapp2.RequestHandler): + def post(self): + name = self.request.get('counter_name') + update_counter(name) + + +class TaskQueueCounterHandler(webapp2.RequestHandler): + """Queues two tasks to increment a counter in global namespace as well as + the namespace is specified by the request, which is arbitrarily named + 'default' if not specified.""" + def get(self, namespace='default'): + # Queue task to update global counter. + current_global_count = get_count('counter') + taskqueue.add( + url='/tasks/counter', + params={'counter_name': 'counter'}) + + # Queue task to update counter in specified namespace. + previous_namespace = namespace_manager.get_namespace() + try: + namespace_manager.set_namespace(namespace) + current_namespace_count = get_count('counter') + taskqueue.add( + url='/tasks/counter', + params={'counter_name': 'counter'}) + finally: + namespace_manager.set_namespace(previous_namespace) + + self.response.write( + 'Counters will be updated asyncronously.' + 'Current values: Global: {}, Namespace {}: {}'.format( + current_global_count, namespace, current_namespace_count)) + + +app = webapp2.WSGIApplication([ + (r'/tasks/counter', DeferredCounterHandler), + (r'/taskqueue', TaskQueueCounterHandler), + (r'/taskqueue/(.*)', TaskQueueCounterHandler) +], debug=True) diff --git a/appengine/standard/multitenancy/taskqueue_test.py b/appengine/standard/multitenancy/taskqueue_test.py new file mode 100644 index 00000000000..636478a41b0 --- /dev/null +++ b/appengine/standard/multitenancy/taskqueue_test.py @@ -0,0 +1,41 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import webtest + +import taskqueue + + +def test_taskqueue(testbed, run_tasks): + app = webtest.TestApp(taskqueue.app) + + response = app.get('/taskqueue') + assert response.status_int == 200 + assert 'Global: 0' in response.body + + run_tasks(app) + + response = app.get('/taskqueue') + assert response.status_int == 200 + assert 'Global: 1' in response.body + + response = app.get('/taskqueue/a') + assert response.status_int == 200 + assert 'a: 0' in response.body + + run_tasks(app) + + response = app.get('/taskqueue/a') + assert response.status_int == 200 + assert 'a: 1' in response.body diff --git a/appengine/standard/ndb/async/README.md b/appengine/standard/ndb/async/README.md new file mode 100644 index 00000000000..745e754e6b2 --- /dev/null +++ b/appengine/standard/ndb/async/README.md @@ -0,0 +1,11 @@ +## App Engine Datastore NDB Asynchronous Operations Samples + +This contains snippets used in the NDB asynchronous operations documentation, +demonstrating various ways to make asynchronous ndb operations. + + +These samples are used on the following documentation page: + +> https://cloud.google.com/appengine/docs/python/ndb/async + + diff --git a/appengine/standard/ndb/async/app_async.py b/appengine/standard/ndb/async/app_async.py new file mode 100644 index 00000000000..c8062cdc4da --- /dev/null +++ b/appengine/standard/ndb/async/app_async.py @@ -0,0 +1,36 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from google.appengine.api import users +from google.appengine.ext import ndb +import webapp2 + + +class Account(ndb.Model): + view_counter = ndb.IntegerProperty() + + +class MyRequestHandler(webapp2.RequestHandler): + def get(self): + acct = Account.get_by_id(users.get_current_user().user_id()) + acct.view_counter += 1 + future = acct.put_async() + + # ...read something else from Datastore... + + self.response.out.write('Content of the page') + future.get_result() + + +app = webapp2.WSGIApplication([('/', MyRequestHandler)]) diff --git a/appengine/standard/ndb/async/app_async_test.py b/appengine/standard/ndb/async/app_async_test.py new file mode 100644 index 00000000000..57c84c18571 --- /dev/null +++ b/appengine/standard/ndb/async/app_async_test.py @@ -0,0 +1,36 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest +import webtest + +import app_async + + +@pytest.fixture +def app(testbed): + return webtest.TestApp(app_async.app) + + +def test_main(app, testbed, login): + app_async.Account(id='123', view_counter=4).put() + + # Log the user in + login(id='123') + + response = app.get('/') + + assert response.status_int == 200 + account = app_async.Account.get_by_id('123') + assert account.view_counter == 5 diff --git a/appengine/standard/ndb/async/app_sync.py b/appengine/standard/ndb/async/app_sync.py new file mode 100644 index 00000000000..8cf53f0c15c --- /dev/null +++ b/appengine/standard/ndb/async/app_sync.py @@ -0,0 +1,35 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from google.appengine.api import users +from google.appengine.ext import ndb +import webapp2 + + +class Account(ndb.Model): + view_counter = ndb.IntegerProperty() + + +class MyRequestHandler(webapp2.RequestHandler): + def get(self): + acct = Account.get_by_id(users.get_current_user().user_id()) + acct.view_counter += 1 + acct.put() + + # ...read something else from Datastore... + + self.response.out.write('Content of the page') + + +app = webapp2.WSGIApplication([('/', MyRequestHandler)]) diff --git a/appengine/standard/ndb/async/app_sync_test.py b/appengine/standard/ndb/async/app_sync_test.py new file mode 100644 index 00000000000..27ed85b68d9 --- /dev/null +++ b/appengine/standard/ndb/async/app_sync_test.py @@ -0,0 +1,36 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest +import webtest + +import app_sync + + +@pytest.fixture +def app(testbed): + return webtest.TestApp(app_sync.app) + + +def test_main(app, testbed, login): + app_sync.Account(id='123', view_counter=4).put() + + # Log the user in + login(id='123') + + response = app.get('/') + + assert response.status_int == 200 + account = app_sync.Account.get_by_id('123') + assert account.view_counter == 5 diff --git a/appengine/standard/ndb/async/app_toplevel/README.md b/appengine/standard/ndb/async/app_toplevel/README.md new file mode 100644 index 00000000000..a234077ecf8 --- /dev/null +++ b/appengine/standard/ndb/async/app_toplevel/README.md @@ -0,0 +1,2 @@ +This is in a separate folder to isolate it from the other apps. +This is necessary because the test won't pass when run with the other tests. diff --git a/appengine/standard/ndb/async/app_toplevel/app_toplevel.py b/appengine/standard/ndb/async/app_toplevel/app_toplevel.py new file mode 100644 index 00000000000..a1a4a2ac774 --- /dev/null +++ b/appengine/standard/ndb/async/app_toplevel/app_toplevel.py @@ -0,0 +1,39 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from google.appengine.api import users +from google.appengine.ext import ndb +import webapp2 + + +class Account(ndb.Model): + view_counter = ndb.IntegerProperty() + + +class MyRequestHandler(webapp2.RequestHandler): + @ndb.toplevel + def get(self): + acct = Account.get_by_id(users.get_current_user().user_id()) + acct.view_counter += 1 + acct.put_async() # Ignoring the Future this returns + + # ...read something else from Datastore... + + self.response.out.write('Content of the page') + + +# This is actually redundant, since the `get` decorator already handles it, but +# for demonstration purposes, you can also make the entire app toplevel with +# the following. +app = ndb.toplevel(webapp2.WSGIApplication([('/', MyRequestHandler)])) diff --git a/appengine/standard/ndb/async/app_toplevel/app_toplevel_test.py b/appengine/standard/ndb/async/app_toplevel/app_toplevel_test.py new file mode 100644 index 00000000000..967a1474451 --- /dev/null +++ b/appengine/standard/ndb/async/app_toplevel/app_toplevel_test.py @@ -0,0 +1,36 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest +import webtest + +import app_toplevel + + +@pytest.fixture +def app(testbed): + return webtest.TestApp(app_toplevel.app) + + +def test_main(app, testbed, login): + app_toplevel.Account(id='123', view_counter=4).put() + + # Log the user in + login(id='123') + + response = app.get('/') + + assert response.status_int == 200 + account = app_toplevel.Account.get_by_id('123') + assert account.view_counter == 5 diff --git a/appengine/standard/ndb/async/app_toplevel/index.html b/appengine/standard/ndb/async/app_toplevel/index.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/appengine/standard/ndb/async/guestbook.py b/appengine/standard/ndb/async/guestbook.py new file mode 100644 index 00000000000..4c2d452c97f --- /dev/null +++ b/appengine/standard/ndb/async/guestbook.py @@ -0,0 +1,101 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from google.appengine.api import users +from google.appengine.ext import ndb +import webapp2 + + +class Guestbook(ndb.Model): + content = ndb.StringProperty() + post_date = ndb.DateTimeProperty(auto_now_add=True) + + +class Account(ndb.Model): + email = ndb.StringProperty() + nickname = ndb.StringProperty() + + def nick(self): + return self.nickname or self.email # Whichever is non-empty + + +class Message(ndb.Model): + text = ndb.StringProperty() + when = ndb.DateTimeProperty(auto_now_add=True) + author = ndb.KeyProperty(kind=Account) # references Account + + +class MainPage(webapp2.RequestHandler): + def get(self): + if self.request.path == '/guestbook': + if self.request.get('async'): + self.get_guestbook_async() + else: + self.get_guestbook_sync() + elif self.request.path == '/messages': + if self.request.get('async'): + self.get_messages_async() + else: + self.get_messages_sync() + + def get_guestbook_sync(self): + uid = users.get_current_user().user_id() + acct = Account.get_by_id(uid) # I/O action 1 + qry = Guestbook.query().order(-Guestbook.post_date) + recent_entries = qry.fetch(10) # I/O action 2 + + # ...render HTML based on this data... + self.response.out.write('{}'.format(''.join( + '

    {}

    '.format(entry.content) for entry in recent_entries))) + + return acct, qry + + def get_guestbook_async(self): + uid = users.get_current_user().user_id() + acct_future = Account.get_by_id_async(uid) # Start I/O action #1 + qry = Guestbook.query().order(-Guestbook.post_date) + recent_entries_future = qry.fetch_async(10) # Start I/O action #2 + acct = acct_future.get_result() # Complete #1 + recent_entries = recent_entries_future.get_result() # Complete #2 + + # ...render HTML based on this data... + self.response.out.write('{}'.format(''.join( + '

    {}

    '.format(entry.content) for entry in recent_entries))) + + return acct, recent_entries + + def get_messages_sync(self): + qry = Message.query().order(-Message.when) + for msg in qry.fetch(20): + acct = msg.author.get() + self.response.out.write( + '

    On {}, {} wrote:'.format(msg.when, acct.nick())) + self.response.out.write('

    {}'.format(msg.text)) + + def get_messages_async(self): + @ndb.tasklet + def callback(msg): + acct = yield msg.author.get_async() + raise ndb.Return('On {}, {} wrote:\n{}'.format( + msg.when, acct.nick(), msg.text)) + + qry = Message.query().order(-Message.when) + outputs = qry.map(callback, limit=20) + for output in outputs: + self.response.out.write('

    {}

    '.format(output)) + + +app = webapp2.WSGIApplication([ + ('/.*', MainPage), +], debug=True) diff --git a/appengine/standard/ndb/async/guestbook_test.py b/appengine/standard/ndb/async/guestbook_test.py new file mode 100644 index 00000000000..e1c51a71476 --- /dev/null +++ b/appengine/standard/ndb/async/guestbook_test.py @@ -0,0 +1,74 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest +import webtest + +import guestbook + + +@pytest.fixture +def app(testbed): + return webtest.TestApp(guestbook.app) + + +def test_get_guestbook_sync(app, testbed, login): + guestbook.Account(id='123').put() + # Log the user in + login(id='123') + + for i in range(11): + guestbook.Guestbook(content='Content {}'.format(i)).put() + + response = app.get('/guestbook') + + assert response.status_int == 200 + assert 'Content 1' in response.body + + +def test_get_guestbook_async(app, testbed, login): + guestbook.Account(id='123').put() + # Log the user in + login(id='123') + for i in range(11): + guestbook.Guestbook(content='Content {}'.format(i)).put() + + response = app.get('/guestbook?async=1') + + assert response.status_int == 200 + assert 'Content 1' in response.body + + +def test_get_messages_sync(app, testbed): + for i in range(21): + account_key = guestbook.Account(nickname='Nick {}'.format(i)).put() + guestbook.Message(author=account_key, text='Text {}'.format(i)).put() + + response = app.get('/messages') + + assert response.status_int == 200 + assert 'Nick 1 wrote:' in response.body + assert '

    Text 1' in response.body + + +def test_get_messages_async(app, testbed): + for i in range(21): + account_key = guestbook.Account(nickname='Nick {}'.format(i)).put() + guestbook.Message(author=account_key, text='Text {}'.format(i)).put() + + response = app.get('/messages?async=1') + + assert response.status_int == 200 + assert 'Nick 1 wrote:' in response.body + assert '\nText 1' in response.body diff --git a/appengine/standard/ndb/async/shopping_cart.py b/appengine/standard/ndb/async/shopping_cart.py new file mode 100644 index 00000000000..1ddf1306d42 --- /dev/null +++ b/appengine/standard/ndb/async/shopping_cart.py @@ -0,0 +1,137 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from google.appengine.ext import ndb + + +# [START models] +class Account(ndb.Model): + pass + + +class InventoryItem(ndb.Model): + name = ndb.StringProperty() + + +class CartItem(ndb.Model): + account = ndb.KeyProperty(kind=Account) + inventory = ndb.KeyProperty(kind=InventoryItem) + quantity = ndb.IntegerProperty() + + +class SpecialOffer(ndb.Model): + inventory = ndb.KeyProperty(kind=InventoryItem) +# [END models] + + +def get_cart_plus_offers(acct): + cart = CartItem.query(CartItem.account == acct.key).fetch() + offers = SpecialOffer.query().fetch(10) + ndb.get_multi([item.inventory for item in cart] + + [offer.inventory for offer in offers]) + return cart, offers + + +def get_cart_plus_offers_async(acct): + cart_future = CartItem.query(CartItem.account == acct.key).fetch_async() + offers_future = SpecialOffer.query().fetch_async(10) + cart = cart_future.get_result() + offers = offers_future.get_result() + ndb.get_multi([item.inventory for item in cart] + + [offer.inventory for offer in offers]) + return cart, offers + + +# [START cart_offers_tasklets] +@ndb.tasklet +def get_cart_tasklet(acct): + cart = yield CartItem.query(CartItem.account == acct.key).fetch_async() + yield ndb.get_multi_async([item.inventory for item in cart]) + raise ndb.Return(cart) + + +@ndb.tasklet +def get_offers_tasklet(acct): + offers = yield SpecialOffer.query().fetch_async(10) + yield ndb.get_multi_async([offer.inventory for offer in offers]) + raise ndb.Return(offers) + + +@ndb.tasklet +def get_cart_plus_offers_tasklet(acct): + cart, offers = yield get_cart_tasklet(acct), get_offers_tasklet(acct) + raise ndb.Return((cart, offers)) +# [END cart_offers_tasklets] + + +@ndb.tasklet +def iterate_over_query_results_in_tasklet(Model, is_the_entity_i_want): + qry = Model.query() + qit = qry.iter() + while (yield qit.has_next_async()): + entity = qit.next() + # Do something with entity + if is_the_entity_i_want(entity): + raise ndb.Return(entity) + + +@ndb.tasklet +def blocking_iteration_over_query_results(Model, is_the_entity_i_want): + # DO NOT DO THIS IN A TASKLET + qry = Model.query() + for entity in qry: + # Do something with entity + if is_the_entity_i_want(entity): + raise ndb.Return(entity) + + +def define_get_google(): + @ndb.tasklet + def get_google(): + context = ndb.get_context() + result = yield context.urlfetch("http://www.google.com/") + if result.status_code == 200: + raise ndb.Return(result.content) + # else return None + + return get_google + + +def define_update_counter_async(): + @ndb.transactional_async + def update_counter(counter_key): + counter = counter_key.get() + counter.value += 1 + counter.put() + return counter.value + + return update_counter + + +def define_update_counter_tasklet(): + @ndb.transactional_tasklet + def update_counter(counter_key): + counter = yield counter_key.get_async() + counter.value += 1 + yield counter.put_async() + + return update_counter + + +def get_first_ready(): + urls = ["http://www.google.com/", "http://www.blogspot.com/"] + context = ndb.get_context() + futures = [context.urlfetch(url) for url in urls] + first_future = ndb.Future.wait_any(futures) + return first_future.get_result().content diff --git a/appengine/standard/ndb/async/shopping_cart_test.py b/appengine/standard/ndb/async/shopping_cart_test.py new file mode 100644 index 00000000000..78f519baba6 --- /dev/null +++ b/appengine/standard/ndb/async/shopping_cart_test.py @@ -0,0 +1,144 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from google.appengine.ext import ndb +import pytest + +import shopping_cart + + +@pytest.fixture +def items(testbed): + account = shopping_cart.Account(id='123') + account.put() + + items = [shopping_cart.InventoryItem(name='Item {}'.format(i)) + for i in range(6)] + special_items = [shopping_cart.InventoryItem(name='Special {}'.format(i)) + for i in range(6)] + for i in items + special_items: + i.put() + + special_offers = [shopping_cart.SpecialOffer(inventory=item.key) + for item in special_items] + cart_items = [ + shopping_cart.CartItem( + account=account.key, inventory=item.key, quantity=i) + for i, item in enumerate(items[:6] + special_items[:6])] + for i in special_offers + cart_items: + i.put() + + return account, items, special_items, cart_items, special_offers + + +def test_get_cart_plus_offers(items): + account, items, special_items, cart_items, special_offers = items + + cart, offers = shopping_cart.get_cart_plus_offers(account) + + assert len(cart) == 12 + assert len(offers) == 6 + + +def test_get_cart_plus_offers_async(items): + account, items, special_items, cart_items, special_offers = items + + cart, offers = shopping_cart.get_cart_plus_offers_async(account) + + assert len(cart) == 12 + assert len(offers) == 6 + + +def test_get_cart_tasklet(items): + account, items, special_items, cart_items, special_offers = items + + future = shopping_cart.get_cart_tasklet(account) + cart = future.get_result() + + assert len(cart) == 12 + + +def test_get_offers_tasklet(items): + account, items, special_items, cart_items, special_offers = items + + future = shopping_cart.get_offers_tasklet(account) + offers = future.get_result() + + assert len(offers) == 6 + + +def test_get_cart_plus_offers_tasklet(items): + account, items, special_items, cart_items, special_offers = items + + future = shopping_cart.get_cart_plus_offers_tasklet( + account) + cart, offers = future.get_result() + + assert len(cart) == 12 + assert len(offers) == 6 + + +def test_iterate_over_query_results_in_tasklet(items): + account, items, special_items, cart_items, special_offers = items + + future = shopping_cart.iterate_over_query_results_in_tasklet( + shopping_cart.InventoryItem, lambda item: '3' in item.name) + + assert '3' in future.get_result().name + + +def test_do_not_iterate_over_tasklet_like_this(items): + account, items, special_items, cart_items, special_offers = items + + future = shopping_cart.blocking_iteration_over_query_results( + shopping_cart.InventoryItem, lambda item: '3' in item.name) + + assert '3' in future.get_result().name + + +def test_get_google(testbed): + testbed.init_urlfetch_stub() + + get_google = shopping_cart.define_get_google() + future = get_google() + assert 'Google' in future.get_result() + + +class Counter(ndb.Model): + value = ndb.IntegerProperty() + + +def test_update_counter_async(testbed): + counter_key = Counter(value=1).put() + update_counter = shopping_cart.define_update_counter_async() + future = update_counter(counter_key) + assert counter_key.get().value == 1 + assert future.get_result() == 2 + assert counter_key.get().value == 2 + + +def test_update_counter_tasklet(testbed): + counter_key = Counter(value=1).put() + update_counter = shopping_cart.define_update_counter_tasklet() + future = update_counter(counter_key) + assert counter_key.get().value == 1 + future.get_result() + assert counter_key.get().value == 2 + + +def test_get_first_ready(testbed): + testbed.init_urlfetch_stub() + + content = shopping_cart.get_first_ready() + assert 'html' in content.lower() diff --git a/appengine/standard/ndb/cache/README.md b/appengine/standard/ndb/cache/README.md new file mode 100644 index 00000000000..a1df62dae0f --- /dev/null +++ b/appengine/standard/ndb/cache/README.md @@ -0,0 +1,11 @@ +## App Engine Datastore NDB Cache Samples + +This contains snippets used in the NDB cache documentation, demonstrating +various operations on ndb caches. + + +These samples are used on the following documentation page: + +> https://cloud.google.com/appengine/docs/python/ndb/cache + + diff --git a/appengine/standard/ndb/cache/snippets.py b/appengine/standard/ndb/cache/snippets.py new file mode 100644 index 00000000000..391ad3f89a0 --- /dev/null +++ b/appengine/standard/ndb/cache/snippets.py @@ -0,0 +1,45 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from google.appengine.ext import ndb + + +def set_in_process_cache_policy(func): + context = ndb.get_context() + context.set_cache_policy(func) + + +def set_memcache_policy(func): + context = ndb.get_context() + context.set_memcache_policy(func) + + +def bypass_in_process_cache_for_account_entities(): + context = ndb.get_context() + context.set_cache_policy(lambda key: key.kind() != 'Account') + + +def set_datastore_policy(func): + context = ndb.get_context() + context.set_datastore_policy(func) + + +def set_memcache_timeout_policy(func): + context = ndb.get_context() + context.set_memcache_timeout_policy(func) + + +def clear_cache(): + context = ndb.get_context() + context.clear_cache() diff --git a/appengine/standard/ndb/cache/snippets_test.py b/appengine/standard/ndb/cache/snippets_test.py new file mode 100644 index 00000000000..257525895ee --- /dev/null +++ b/appengine/standard/ndb/cache/snippets_test.py @@ -0,0 +1,60 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from google.appengine.ext import ndb + +import snippets + + +def test_set_in_process_cache_policy(testbed): + def policy(key): + return 1 == 1 + + snippets.set_in_process_cache_policy(policy) + assert policy == ndb.get_context().get_cache_policy() + + +def test_set_memcache_policy(testbed): + def policy(key): + return 1 == 2 + + snippets.set_memcache_policy(policy) + assert policy == ndb.get_context().get_memcache_policy() + + +def test_bypass_in_process_cache_for_account_entities(testbed): + context = ndb.get_context() + assert context.get_cache_policy() == context.default_cache_policy + snippets.bypass_in_process_cache_for_account_entities() + assert context.get_cache_policy() != context.default_cache_policy + + +def test_set_datastore_policy(testbed): + def policy(key): + return key is None + + snippets.set_datastore_policy(policy) + assert ndb.get_context().get_datastore_policy() == policy + + +def test_set_memcache_timeout_policy(testbed): + def policy(key): + return 1 + + snippets.set_memcache_timeout_policy(policy) + assert ndb.get_context().get_memcache_timeout_policy() == policy + + +def test_clear_cache(testbed): + snippets.clear_cache() diff --git a/appengine/standard/ndb/entities/README.md b/appengine/standard/ndb/entities/README.md new file mode 100644 index 00000000000..2327d33fcc6 --- /dev/null +++ b/appengine/standard/ndb/entities/README.md @@ -0,0 +1,15 @@ +## App Engine Datastore NDB Entities Samples + +This contains snippets used in the NDB entity documentation, demonstrating +various operations on ndb entities. + + +These samples are used on the following documentation pages: + +> +* https://cloud.google.com/appengine/docs/python/datastore/creating-entities +* https://cloud.google.com/appengine/docs/python/datastore/creating-entity-models +* https://cloud.google.com/appengine/docs/python/users/userobjects +* https://cloud.google.com/appengine/docs/python/datastore/creating-entity-keys + + diff --git a/appengine/standard/ndb/entities/snippets.py b/appengine/standard/ndb/entities/snippets.py new file mode 100644 index 00000000000..e42705b9da1 --- /dev/null +++ b/appengine/standard/ndb/entities/snippets.py @@ -0,0 +1,294 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from google.appengine.ext import ndb + + +class Account(ndb.Model): + username = ndb.StringProperty() + userid = ndb.IntegerProperty() + email = ndb.StringProperty() + + +def create_entity_using_keyword_arguments(): + sandy = Account( + username='Sandy', userid=123, email='sandy@example.com') + return sandy + + +def create_entity_using_attributes(): + sandy = Account() + sandy.username = 'Sandy' + sandy.userid = 123 + sandy.email = 'sandy@example.com' + return sandy + + +def create_entity_using_populate(): + sandy = Account() + sandy.populate( + username='Sandy', + userid=123, + email='sandy@gmail.com') + return sandy + + +def demonstrate_model_constructor_type_checking(): + bad = Account( + username='Sandy', userid='not integer') # raises an exception + return bad + + +def demonstrate_entity_attribute_type_checking(sandy): + sandy.username = 42 # raises an exception + + +def save_entity(sandy): + sandy_key = sandy.put() + return sandy_key + + +def get_entity(sandy_key): + sandy = sandy_key.get() + return sandy + + +def get_key_kind_and_id(sandy_key): + kind_string = sandy_key.kind() # returns 'Account' + ident = sandy_key.id() # returns '2' + return kind_string, ident + + +def get_url_safe_key(sandy_key): + url_string = sandy_key.urlsafe() + return url_string + + +def get_entity_from_url_safe_key(url_string): + sandy_key = ndb.Key(urlsafe=url_string) + sandy = sandy_key.get() + return sandy + + +def get_key_and_numeric_id_from_url_safe_key(url_string): + key = ndb.Key(urlsafe=url_string) + kind_string = key.kind() + ident = key.id() + return key, ident, kind_string + + +def update_entity_from_key(key): + sandy = key.get() + sandy.email = 'sandy@example.co.uk' + sandy.put() + + +def delete_entity(sandy): + sandy.key.delete() + + +def create_entity_with_named_key(): + account = Account( + username='Sandy', userid=1234, email='sandy@example.com', + id='sandy@example.com') + + return account.key.id() # returns 'sandy@example.com' + + +def set_key_directly(account): + account.key = ndb.Key('Account', 'sandy@example.com') + + # You can also use the model class object itself, rather than its name, + # to specify the entity's kind: + account.key = ndb.Key(Account, 'sandy@example.com') + + +def create_entity_with_generated_id(): + # note: no id kwarg + account = Account(username='Sandy', userid=1234, email='sandy@example.com') + account.put() + # Account.key will now have a key of the form: ndb.Key(Account, 71321839) + # where the value 71321839 was generated by Datastore for us. + return account + + +class Revision(ndb.Model): + message_text = ndb.StringProperty() + + +def demonstrate_entities_with_parent_hierarchy(): + ndb.Key('Account', 'sandy@example.com', 'Message', 123, 'Revision', '1') + ndb.Key('Account', 'sandy@example.com', 'Message', 123, 'Revision', '2') + ndb.Key('Account', 'larry@example.com', 'Message', 456, 'Revision', '1') + ndb.Key('Account', 'larry@example.com', 'Message', 789, 'Revision', '2') + + +def equivalent_ways_to_define_key_with_parent(): + ndb.Key('Account', 'sandy@example.com', 'Message', 123, 'Revision', '1') + + ndb.Key('Revision', '1', parent=ndb.Key( + 'Account', 'sandy@example.com', 'Message', 123)) + + ndb.Key('Revision', '1', parent=ndb.Key( + 'Message', 123, parent=ndb.Key('Account', 'sandy@example.com'))) + + +def create_root_key(): + sandy_key = ndb.Key(Account, 'sandy@example.com') + return sandy_key + + +def create_entity_with_parent_keys(): + account_key = ndb.Key(Account, 'sandy@example.com') + + # Ask Datastore to allocate an ID. + new_id = ndb.Model.allocate_ids(size=1, parent=account_key)[0] + + # Datastore returns us an integer ID that we can use to create the message + # key + message_key = ndb.Key('Message', new_id, parent=account_key) + + # Now we can put the message into Datastore + initial_revision = Revision( + message_text='Hello', id='1', parent=message_key) + initial_revision.put() + + return initial_revision + + +def get_parent_key_of_entity(initial_revision): + message_key = initial_revision.key.parent() + return message_key + + +def operate_on_multiple_keys_at_once(list_of_entities): + list_of_keys = ndb.put_multi(list_of_entities) + list_of_entities = ndb.get_multi(list_of_keys) + ndb.delete_multi(list_of_keys) + + +class Mine(ndb.Expando): + pass + + +def create_entity_using_expando_model(): + e = Mine() + e.foo = 1 + e.bar = 'blah' + e.tags = ['exp', 'and', 'oh'] + e.put() + + return e + + +def get_properties_defined_on_expando(e): + return e._properties + # { + # 'foo': GenericProperty('foo'), + # 'bar': GenericProperty('bar'), + # 'tags': GenericProperty('tags', repeated=True) + # } + + +class FlexEmployee(ndb.Expando): + name = ndb.StringProperty() + age = ndb.IntegerProperty() + + +def create_expando_model_entity_with_defined_properties(): + employee = FlexEmployee(name='Sandy', location='SF') + return employee + + +class Specialized(ndb.Expando): + _default_indexed = False + + +def create_expando_model_entity_that_isnt_indexed_by_default(): + e = Specialized(foo='a', bar=['b']) + return e._properties + # { + # 'foo': GenericProperty('foo', indexed=False), + # 'bar': GenericProperty('bar', indexed=False, repeated=True) + # } + + +def demonstrate_wrong_way_to_query_expando(): + FlexEmployee.query(FlexEmployee.location == 'SF') + + +def demonstrate_right_way_to_query_expando(): + FlexEmployee.query(ndb.GenericProperty('location') == 'SF') + + +notification = None + + +def _notify(message): + global notification + notification = message + + +class Friend(ndb.Model): + name = ndb.StringProperty() + + def _pre_put_hook(self): + _notify('Gee wiz I have a new friend!') + + @classmethod + def _post_delete_hook(cls, key, future): + _notify('I have found occasion to rethink our friendship.') + + +def demonstrate_model_put_and_delete_hooks(): + f = Friend() + f.name = 'Carole King' + f.put() # _pre_put_hook is called + yield f + fut = f.key.delete_async() # _post_delete_hook not yet called + fut.get_result() # _post_delete_hook is called + yield f + + +class MyModel(ndb.Model): + pass + + +def reserve_model_ids(): + first, last = MyModel.allocate_ids(100) + return first, last + + +def reserve_model_ids_with_a_parent(p): + first, last = MyModel.allocate_ids(100, parent=p) + return first, last + + +def construct_keys_from_range_of_reserved_ids(first, last): + keys = [ndb.Key(MyModel, id) for id in range(first, last+1)] + return keys + + +def reserve_model_ids_up_to(N): + first, last = MyModel.allocate_ids(max=N) + return first, last + + +class ModelWithUser(ndb.Model): + user_id = ndb.StringProperty() + color = ndb.StringProperty() + + @classmethod + def get_by_user(cls, user): + return cls.query().filter(cls.user_id == user.user_id()).get() diff --git a/appengine/standard/ndb/entities/snippets_test.py b/appengine/standard/ndb/entities/snippets_test.py new file mode 100644 index 00000000000..e93ffc6b81a --- /dev/null +++ b/appengine/standard/ndb/entities/snippets_test.py @@ -0,0 +1,228 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from google.appengine.api import users +from google.appengine.ext import ndb +from google.appengine.ext.ndb.google_imports import datastore_errors +import pytest + +import snippets + + +def test_create_entity_using_keyword_arguments(testbed): + result = snippets.create_entity_using_keyword_arguments() + assert isinstance(result, snippets.Account) + + +def test_create_entity_using_attributes(testbed): + result = snippets.create_entity_using_attributes() + assert isinstance(result, snippets.Account) + + +def test_create_entity_using_populate(testbed): + result = snippets.create_entity_using_populate() + assert isinstance(result, snippets.Account) + + +def test_demonstrate_model_constructor_type_checking(testbed): + with pytest.raises(datastore_errors.BadValueError): + snippets.demonstrate_model_constructor_type_checking() + + +def test_demonstrate_entity_attribute_type_checking(testbed): + with pytest.raises(datastore_errors.BadValueError): + snippets.demonstrate_entity_attribute_type_checking( + snippets.create_entity_using_keyword_arguments()) + + +def test_save_entity(testbed): + result = snippets.save_entity( + snippets.create_entity_using_keyword_arguments()) + assert isinstance(result, snippets.ndb.Key) + + +def test_get_entity(testbed): + sandy_key = snippets.save_entity( + snippets.create_entity_using_keyword_arguments()) + result = snippets.get_entity(sandy_key) + assert isinstance(result, snippets.Account) + + +def test_get_key_kind_and_id(testbed): + sandy_key = snippets.save_entity( + snippets.create_entity_using_keyword_arguments()) + kind_string, ident = snippets.get_key_kind_and_id(sandy_key) + assert kind_string == 'Account' + assert isinstance(ident, long) + + +def test_get_url_safe_key(testbed): + sandy_key = snippets.save_entity( + snippets.create_entity_using_keyword_arguments()) + result = snippets.get_url_safe_key(sandy_key) + assert isinstance(result, str) + + +def test_get_entity_from_url_safe_key(testbed): + sandy_key = snippets.save_entity( + snippets.create_entity_using_keyword_arguments()) + result = snippets.get_entity_from_url_safe_key( + snippets.get_url_safe_key(sandy_key)) + assert isinstance(result, snippets.Account) + assert result.username == 'Sandy' + + +def test_get_key_and_numeric_id_from_url_safe_key(testbed): + sandy_key = snippets.save_entity( + snippets.create_entity_using_keyword_arguments()) + urlsafe = snippets.get_url_safe_key(sandy_key) + key, ident, kind_string = ( + snippets.get_key_and_numeric_id_from_url_safe_key(urlsafe)) + assert isinstance(key, ndb.Key) + assert isinstance(ident, long) + assert isinstance(kind_string, str) + + +def test_update_entity_from_key(testbed): + sandy = snippets.create_entity_using_keyword_arguments() + sandy_key = snippets.save_entity(sandy) + urlsafe = snippets.get_url_safe_key(sandy_key) + key, ident, kind_string = ( + snippets.get_key_and_numeric_id_from_url_safe_key(urlsafe)) + snippets.update_entity_from_key(key) + assert key.get().email == 'sandy@example.co.uk' + + +def test_delete_entity(testbed): + sandy = snippets.create_entity_using_keyword_arguments() + snippets.save_entity(sandy) + snippets.delete_entity(sandy) + assert sandy.key.get() is None + + +def test_create_entity_with_named_key(testbed): + result = snippets.create_entity_with_named_key() + assert 'sandy@example.com' == result + + +def test_set_key_directly(testbed): + account = snippets.Account() + snippets.set_key_directly(account) + assert account.key.id() == 'sandy@example.com' + + +def test_create_entity_with_generated_id(testbed): + result = snippets.create_entity_with_generated_id() + assert isinstance(result.key.id(), long) + + +def test_demonstrate_entities_with_parent_hierarchy(testbed): + snippets.demonstrate_entities_with_parent_hierarchy() + + +def test_equivalent_ways_to_define_key_with_parent(testbed): + snippets.equivalent_ways_to_define_key_with_parent() + + +def test_create_root_key(testbed): + result = snippets.create_root_key() + assert result.id() == 'sandy@example.com' + + +def test_create_entity_with_parent_keys(testbed): + result = snippets.create_entity_with_parent_keys() + assert result.message_text == 'Hello' + + +def test_get_parent_key_of_entity(testbed): + initial_revision = snippets.create_entity_with_parent_keys() + result = snippets.get_parent_key_of_entity(initial_revision) + assert result.kind() == 'Message' + + +def test_operate_on_multiple_keys_at_once(testbed): + snippets.operate_on_multiple_keys_at_once([ + snippets.Account(email='a@a.com'), snippets.Account(email='b@b.com')]) + + +def test_create_entity_using_expando_model(testbed): + result = snippets.create_entity_using_expando_model() + assert result.foo == 1 + + +def test_get_properties_defined_on_expando(testbed): + result = snippets.get_properties_defined_on_expando( + snippets.create_entity_using_expando_model()) + assert result['foo'] is not None + assert result['bar'] is not None + assert result['tags'] is not None + + +def test_create_entity_using_expando_model_with_defined_properties(testbed): + result = snippets.create_expando_model_entity_with_defined_properties() + assert result.name == 'Sandy' + + +def test_create_expando_model_entity_that_isnt_indexed_by_default(testbed): + result = ( + snippets.create_expando_model_entity_that_isnt_indexed_by_default()) + assert result['foo'] + assert result['bar'] + + +def test_demonstrate_wrong_way_to_query_expando(testbed): + with pytest.raises(AttributeError): + snippets.demonstrate_wrong_way_to_query_expando() + + +def test_demonstrate_right_way_to_query_expando(testbed): + snippets.demonstrate_right_way_to_query_expando() + + +def test_demonstrate_model_put_and_delete_hooks(testbed): + iterator = snippets.demonstrate_model_put_and_delete_hooks() + iterator.next() + assert snippets.notification == 'Gee wiz I have a new friend!' + iterator.next() + assert snippets.notification == ( + 'I have found occasion to rethink our friendship.') + + +def test_reserve_model_ids(testbed): + first, last = snippets.reserve_model_ids() + assert last - first >= 99 + + +def test_reserve_model_ids_with_a_parent(testbed): + first, last = snippets.reserve_model_ids_with_a_parent( + snippets.Friend().key) + assert last - first >= 99 + + +def test_construct_keys_from_range_of_reserved_ids(testbed): + result = snippets.construct_keys_from_range_of_reserved_ids( + *snippets.reserve_model_ids()) + assert len(result) == 100 + + +def test_reserve_model_ids_up_to(testbed): + first, last = snippets.reserve_model_ids_up_to(5) + assert last - first >= 4 + + +def test_model_with_user(testbed): + user = users.User(email='user@example.com', _user_id='123') + item = snippets.ModelWithUser(user_id=user.user_id()) + item.put() + assert snippets.ModelWithUser.get_by_user(user) == item diff --git a/appengine/standard/ndb/modeling/README.md b/appengine/standard/ndb/modeling/README.md new file mode 100644 index 00000000000..85fd2a34dab --- /dev/null +++ b/appengine/standard/ndb/modeling/README.md @@ -0,0 +1,5 @@ +## App Engine Datastore NDB Modeling Samples + +These samples demonstrate how to [model entity relationships](https://cloud.google.com/appengine/articles/modeling) using the Datastore NDB library. + +Refer to the [App Engine Samples README](../../README.md) for information on how to run and deploy this sample. diff --git a/appengine/standard/ndb/modeling/contact_with_group_models.py b/appengine/standard/ndb/modeling/contact_with_group_models.py new file mode 100644 index 00000000000..16977c4fdae --- /dev/null +++ b/appengine/standard/ndb/modeling/contact_with_group_models.py @@ -0,0 +1,84 @@ +# Copyright 2014 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Models for representing an address book contact with grouping. + +This module provides models which implement structured properties, +strongly consistent querying, and one-to-many grouping. + +For more information, see README.md. +""" + + +from google.appengine.ext import ndb + + +# [START contact_with_group_models] +class PhoneNumber(ndb.Model): + """A model representing a phone number.""" + phone_type = ndb.StringProperty( + choices=('home', 'work', 'fax', 'mobile', 'other')) + number = ndb.StringProperty() + + +class Group(ndb.Model): + """A model representing groups. + + Expects to have a parent key with the id of the addressbook owner. + + Example: assume my username is tmatsuo + + addrbook_key = ndb.Key('AddressBook', 'tmatsuo') + friends = models.Group(parent=addrbook_key, name="friends") + friends.put() + """ + name = ndb.StringProperty() + description = ndb.TextProperty() + + @property + def members(self): + """Returns a query object with myself as an ancestor.""" + return Contact.query(ancestor=self.key.parent()).filter( + Contact.groups == self.key) + + +class Contact(ndb.Model): + """A Contact model that uses repeated KeyProperty. + + Expects to have a parent key with the id of the addressbook owner. + + Example: assume my username is tmatsuo + + addrbook_key = ndb.Key('AddressBook', 'tmatsuo') + mary = models.Contact(parent=addrbook_key, name='mary', ...) + mary.put() + """ + # Basic info. + name = ndb.StringProperty() + birth_day = ndb.DateProperty() + + # Address info. + address = ndb.StringProperty() + + phone_numbers = ndb.StructuredProperty(PhoneNumber, repeated=True) + + # Company info. + company_title = ndb.StringProperty() + company_name = ndb.StringProperty() + company_description = ndb.TextProperty() + company_address = ndb.StringProperty() + + # Group affiliation + groups = ndb.KeyProperty(Group, repeated=True) +# [END contact_with_group_models] diff --git a/appengine/standard/ndb/modeling/contact_with_group_models_test.py b/appengine/standard/ndb/modeling/contact_with_group_models_test.py new file mode 100644 index 00000000000..1afe71682df --- /dev/null +++ b/appengine/standard/ndb/modeling/contact_with_group_models_test.py @@ -0,0 +1,46 @@ +# Copyright 2014 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from google.appengine.ext import ndb + +import contact_with_group_models as models + + +def test_models(testbed): + # Creates 3 contacts and 1 group. + # Assuming the group and contacts are private and belong to tmatsuo's + # addressbook. + addressbook_key = ndb.Key('AddressBook', 'tmatsuo') + + friends = models.Group(parent=addressbook_key, name='friends') + friends.put() + friends_key = friends.key + mary = models.Contact(parent=addressbook_key, name='Mary') + mary.put() + mary_key = mary.key + + # Add Mary to your 'friends' group + mary = mary_key.get() + friends = friends_key.get() + if friends.key not in mary.groups: + mary.groups.append(friends.key) + mary.put() + + # Now Mary is your friend + mary = mary_key.get() + assert friends.key in mary.groups + + # How about 'members' property? + friend_list = friends.members.fetch() + assert len(friend_list) == 1 diff --git a/appengine/standard/ndb/modeling/keyproperty_models.py b/appengine/standard/ndb/modeling/keyproperty_models.py new file mode 100644 index 00000000000..c179e85e01d --- /dev/null +++ b/appengine/standard/ndb/modeling/keyproperty_models.py @@ -0,0 +1,64 @@ +# Copyright 2014 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +"""Models for representing a contact with multiple phone numbers. + +This module provides models with a relationship with ndb.KeyProperty to +allow a single contact to have multiple phone numbers. + +For more information, see README.md. +""" + + +# In the original article, it uses ReferenceProperty on the +# PhoneNumber model. With ndb, there is no ReferenceProperty any more, +# so here we use KeyProperty first. However this pattern has a +# consistency issue, shown in the test_fails function in +# test/test_keyproperty_models.py. + + +from google.appengine.ext import ndb + + +# [START keyproperty_models] +class Contact(ndb.Model): + """A Contact model with KeyProperty.""" + # Basic info. + name = ndb.StringProperty() + birth_day = ndb.DateProperty() + + # Address info. + address = ndb.StringProperty() + + # Company info. + company_title = ndb.StringProperty() + company_name = ndb.StringProperty() + company_description = ndb.TextProperty() + company_address = ndb.StringProperty() + + # The original phone_number property has been replaced by + # the following property. + @property + def phone_numbers(self): + return PhoneNumber.query(PhoneNumber.contact == self.key) + + +class PhoneNumber(ndb.Model): + """A model representing a phone number.""" + contact = ndb.KeyProperty(Contact) + phone_type = ndb.StringProperty( + choices=('home', 'work', 'fax', 'mobile', 'other')) + number = ndb.StringProperty() +# [END keyproperty_models] diff --git a/appengine/standard/ndb/modeling/keyproperty_models_test.py b/appengine/standard/ndb/modeling/keyproperty_models_test.py new file mode 100644 index 00000000000..a2a4d5a3bd9 --- /dev/null +++ b/appengine/standard/ndb/modeling/keyproperty_models_test.py @@ -0,0 +1,45 @@ +# Copyright 2014 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Test classes for code snippet for modeling article.""" + +import pytest + +import keyproperty_models as models + + +def test_models(testbed): + name = 'Takashi Matsuo' + contact = models.Contact(name=name) + contact.put() + contact = contact.key.get() + assert contact.name == name + + +# This test fails because of the eventual consistency nature of +# HRD. We configure HRD consistency for the test datastore stub to +# match the production behavior. +@pytest.mark.xfail +# [START failing_test] +def test_fails(self): + contact = models.Contact(name='Example') + contact.put() + + models.PhoneNumber( + contact=self.contact_key, + phone_type='home', + number='(650) 555 - 2200').put() + numbers = contact.phone_numbers.fetch() + assert 1 == len(numbers) +# [END failing_test] diff --git a/appengine/standard/ndb/modeling/naive_models.py b/appengine/standard/ndb/modeling/naive_models.py new file mode 100644 index 00000000000..7a9f30842c9 --- /dev/null +++ b/appengine/standard/ndb/modeling/naive_models.py @@ -0,0 +1,47 @@ +# Copyright 2014 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""A simple model for representing an address book contact. + +For more information, see README.md. +""" + +# This is the first naive model for starting the article. In the +# older version of this article with db module, we used +# PhoneNumberProperty and PostalAddressProperty. With ndb, there are +# no properties like those, so we just use StringProperty instead. + + +from google.appengine.ext import ndb + + +# [START naive_models] +class Contact(ndb.Model): + """A naive Contact model.""" + # Basic info. + name = ndb.StringProperty() + birth_day = ndb.DateProperty() + + # Address info. + address = ndb.StringProperty() + + # Phone info. + phone_number = ndb.StringProperty() + + # Company info. + company_title = ndb.StringProperty() + company_name = ndb.StringProperty() + company_description = ndb.TextProperty() + company_address = ndb.StringProperty() +# [END naive_models] diff --git a/appengine/standard/ndb/modeling/naive_models_test.py b/appengine/standard/ndb/modeling/naive_models_test.py new file mode 100644 index 00000000000..7b6414673e3 --- /dev/null +++ b/appengine/standard/ndb/modeling/naive_models_test.py @@ -0,0 +1,25 @@ +# Copyright 2014 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Test classes for code snippet for modeling article.""" + +import naive_models as models + + +def test_models(testbed): + name = 'Takashi Matsuo' + contact = models.Contact(name=name) + contact.put() + contact = contact.key.get() + assert contact.name == name diff --git a/appengine/standard/ndb/modeling/parent_child_models.py b/appengine/standard/ndb/modeling/parent_child_models.py new file mode 100644 index 00000000000..0b28dabad04 --- /dev/null +++ b/appengine/standard/ndb/modeling/parent_child_models.py @@ -0,0 +1,63 @@ +# Copyright 2014 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Models representing a contact with multiple phone numbers. + +This module shows a parent-child relationship implemented via entity groups +that allows strongly consistent querying. + +For more information, see README.md. +""" + + +# In the original article, it uses ReferenceProperty on the +# PhoneNumber model. With ndb, there is no ReferenceProperty any more, +# so here we use Parent/Child relationship instead. + + +from google.appengine.ext import ndb + + +# [START parent_child_models] +class Contact(ndb.Model): + """A Contact model with Parent/Child relationship.""" + # Basic info. + name = ndb.StringProperty() + birth_day = ndb.DateProperty() + + # Address info. + address = ndb.StringProperty() + + # Company info. + company_title = ndb.StringProperty() + company_name = ndb.StringProperty() + company_description = ndb.TextProperty() + company_address = ndb.StringProperty() + + # The original phone_number property has been replaced by + # the following property. + @property + def phone_numbers(self): + return PhoneNumber.query(ancestor=self.key) + + +class PhoneNumber(ndb.Model): + """A model representing a phone number. + + Expects to have Contact's key as the parent key. + """ + phone_type = ndb.StringProperty( + choices=('home', 'work', 'fax', 'mobile', 'other')) + number = ndb.StringProperty() +# [END parent_child_models] diff --git a/appengine/standard/ndb/modeling/parent_child_models_test.py b/appengine/standard/ndb/modeling/parent_child_models_test.py new file mode 100644 index 00000000000..49afc670c57 --- /dev/null +++ b/appengine/standard/ndb/modeling/parent_child_models_test.py @@ -0,0 +1,82 @@ +# Copyright 2014 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Test classes for code snippet for modeling article.""" + +from google.appengine.ext import ndb +import pytest + +import parent_child_models as models + + +NAME = 'Takashi Matsuo' + + +@pytest.fixture +def contact_key(testbed): + contact = models.Contact(name=NAME) + contact.put() + return contact.key + + +def test_basic(contact_key): + contact = contact_key.get() + assert contact.name == NAME + + +# [START succeeding_test] +def test_success(contact_key): + contact = contact_key.get() + models.PhoneNumber(parent=contact_key, + phone_type='home', + number='(650) 555 - 2200').put() + numbers = contact.phone_numbers.fetch() + assert 1 == len(numbers) +# [START succeeding_test] + + +def test_phone_numbers(contact_key): + """A test for 'phone_numbers' property.""" + models.PhoneNumber(parent=contact_key, + phone_type='home', + number='(650) 555 - 2200').put() + models.PhoneNumber(parent=contact_key, + phone_type='mobile', + number='(650) 555 - 2201').put() + contact = contact_key.get() + for phone in contact.phone_numbers: + # it doesn't ensure any order + if phone.phone_type == 'home': + assert '(650) 555 - 2200' == phone.number + elif phone.phone_type == 'mobile': + assert phone.number == '(650) 555 - 2201' + + # filer the phone numbers by type. Note that this is an + # ancestor query. + query = contact.phone_numbers.filter( + models.PhoneNumber.phone_type == 'home') + entities = query.fetch() + assert 1 == len(entities) + assert entities[0].number == '(650) 555 - 2200' + + # delete the mobile phones + query = contact.phone_numbers.filter( + models.PhoneNumber.phone_type == 'mobile') + ndb.delete_multi([e.key for e in query]) + + # make sure there's no mobile phones any more + query = contact.phone_numbers.filter( + models.PhoneNumber.phone_type == 'mobile') + entities = query.fetch() + assert 0 == len(entities) diff --git a/appengine/standard/ndb/modeling/relation_model_models.py b/appengine/standard/ndb/modeling/relation_model_models.py new file mode 100644 index 00000000000..9b1e51f9f66 --- /dev/null +++ b/appengine/standard/ndb/modeling/relation_model_models.py @@ -0,0 +1,112 @@ +# Copyright 2014 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Models for representing a contact with multiple titles and groups. + +This module provides models with structured properties, strong +consistent querying, one-to-many grouping, many-to-many relationships. + +For more information, see README.md. +""" + + +from google.appengine.ext import ndb + + +# [START relation_model_models] +class PhoneNumber(ndb.Model): + """A model representing a phone number.""" + phone_type = ndb.StringProperty( + choices=('home', 'work', 'fax', 'mobile', 'other')) + number = ndb.StringProperty() + + +class Group(ndb.Model): + """A model representing groups. + + Expects to have a parent key with the id of the addressbook owner. + + Example: assume my username is tmatsuo + + addrbook_key = ndb.Key('AddressBook', 'tmatsuo') + friends = models.Group(parent=addrbook_key, name="friends") + friends.put() + """ + name = ndb.StringProperty() + description = ndb.TextProperty() + + @property + def members(self): + """Returns a query object with myself as an ancestor.""" + return Contact.query(ancestor=self.key.parent()).filter( + Contact.groups == self.key) + + +class Contact(ndb.Model): + """A model representing a contact entry. + + Expects to have a parent key with the id of the addressbook owner. + + Example: assume my username is tmatsuo + + addrbook_key = ndb.Key('AddressBook', 'tmatsuo') + mary = models.Contact(parent=addrbook_key, name='Mary', ...) + mary.put() + """ + # Basic info. + name = ndb.StringProperty() + birth_day = ndb.DateProperty() + + # Address info. + address = ndb.StringProperty() + + phone_numbers = ndb.StructuredProperty(PhoneNumber, repeated=True) + + # Group affiliation + groups = ndb.KeyProperty(Group, repeated=True) + + # The original organization properties have been replaced by + # the following property. + @property + def companies(self): + rels = ContactCompany.query(ancestor=self.key.parent()).filter( + ContactCompany.contact == self.key) + keys = [rel.company for rel in rels] + return ndb.get_multi(keys) + + +class Company(ndb.Model): + """A model representing a company.""" + name = ndb.StringProperty() + description = ndb.TextProperty() + company_address = ndb.StringProperty() + + +class ContactCompany(ndb.Model): + """A model representing a relation between a contact and a company. + + Expects to have a parent key with the id of the addressbook owner. + + Example: assume my username is tmatsuo + + addrbook_key = ndb.Key('AddressBook', 'tmatsuo') + mary = models.Contact(parent=addrbook_key, name='Mary', ...) + mary.put() + models.ContactCompany(parent=addrbook_key, contact=mary.key, + company=google.key, title='engineer').put() + """ + contact = ndb.KeyProperty(Contact, required=True) + company = ndb.KeyProperty(Company, required=True) + title = ndb.StringProperty() +# [END relation_model_models] diff --git a/appengine/standard/ndb/modeling/relation_model_models_test.py b/appengine/standard/ndb/modeling/relation_model_models_test.py new file mode 100644 index 00000000000..8310132d133 --- /dev/null +++ b/appengine/standard/ndb/modeling/relation_model_models_test.py @@ -0,0 +1,44 @@ +# Copyright 2014 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Test classes for code snippet for modeling article.""" + +from google.appengine.ext import ndb + +import relation_model_models as models + + +def test_relationship(testbed): + # Creates 1 contact and 2 companies + addressbook_key = ndb.Key('AddressBook', 'tmatsuo') + mary = models.Contact(parent=addressbook_key, name='Mary') + mary.put() + google = models.Company(name='Google') + google.put() + candit = models.Company(name='Candit') + candit.put() + + # first google hires Mary + models.ContactCompany(parent=addressbook_key, + contact=mary.key, + company=google.key, + title='engineer').put() + # then another company named 'candit' hires Mary too + models.ContactCompany(parent=addressbook_key, + contact=mary.key, + company=candit.key, + title='president').put() + + # get the list of companies that Mary belongs to + assert len(mary.companies) == 2 diff --git a/appengine/standard/ndb/modeling/structured_property_models.py b/appengine/standard/ndb/modeling/structured_property_models.py new file mode 100644 index 00000000000..33682afdc9c --- /dev/null +++ b/appengine/standard/ndb/modeling/structured_property_models.py @@ -0,0 +1,51 @@ +# Copyright 2014 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Models for representing a contact with multiple phone numbers. + +This module provides models that represent a one-to-many relationship by +embedding a list of related entites using NDB's StructuredProperty. + +For more information, see README.md. +""" + + +from google.appengine.ext import ndb + + +# [START structured_property_models] +class PhoneNumber(ndb.Model): + """A model representing a phone number.""" + phone_type = ndb.StringProperty( + choices=('home', 'work', 'fax', 'mobile', 'other')) + number = ndb.StringProperty() + + +class Contact(ndb.Model): + """A Contact model that uses StructuredProperty for phone numbers.""" + # Basic info. + name = ndb.StringProperty() + birth_day = ndb.DateProperty() + + # Address info. + address = ndb.StringProperty() + + phone_numbers = ndb.StructuredProperty(PhoneNumber, repeated=True) + + # Company info. + company_title = ndb.StringProperty() + company_name = ndb.StringProperty() + company_description = ndb.TextProperty() + company_address = ndb.StringProperty() +# [END structured_property_models] diff --git a/appengine/standard/ndb/modeling/structured_property_models_test.py b/appengine/standard/ndb/modeling/structured_property_models_test.py new file mode 100644 index 00000000000..41f5e5bf695 --- /dev/null +++ b/appengine/standard/ndb/modeling/structured_property_models_test.py @@ -0,0 +1,58 @@ +# Copyright 2014 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Test classes for code snippet for modeling article.""" + +import structured_property_models as models + + +def test_phone_numbers(testbed): + # Create one Contact entity with 2 phone numbers. + scott = models.Contact(name='scott') + scott.phone_numbers.append( + models.PhoneNumber(phone_type='home', + number='(650) 555 - 2200')) + scott.phone_numbers.append( + models.PhoneNumber(phone_type='mobile', + number='(650) 555 - 2201')) + scott.put() + + # make sure there are 2 numbers, you can expect the order is preserved. + assert len(scott.phone_numbers) == 2 + assert scott.phone_numbers[0].phone_type == 'home' + assert scott.phone_numbers[0].number == '(650) 555 - 2200' + assert scott.phone_numbers[1].phone_type == 'mobile' + assert scott.phone_numbers[1].number == '(650) 555 - 2201' + + # filer scott's phone numbers by type + home_numbers = [phone_number for phone_number in scott.phone_numbers + if phone_number.phone_type == 'home'] + assert len(home_numbers) == 1 + assert home_numbers[0].number == '(650) 555 - 2200' + + # delete scott's mobile phone + mobile_numbers = [phone_number for phone_number in scott.phone_numbers + if phone_number.phone_type == 'mobile'] + assert len(mobile_numbers) == 1 + lost_phone = mobile_numbers[0] + scott.phone_numbers.remove(lost_phone) + + # Updates the entity (resending all its properties over the wire). + scott.put() + + # make sure there's no mobile phone of scott + scott = scott.key.get() + assert len(scott.phone_numbers) == 1 + assert scott.phone_numbers[0].phone_type == 'home' + assert scott.phone_numbers[0].number == '(650) 555 - 2200' diff --git a/appengine/standard/ndb/overview/README.md b/appengine/standard/ndb/overview/README.md new file mode 100644 index 00000000000..35d6b218e69 --- /dev/null +++ b/appengine/standard/ndb/overview/README.md @@ -0,0 +1,12 @@ +## App Engine Datastore NDB Overview Sample + +This is a sample app for Google App Engine that demonstrates the [Datastore NDB Python API](https://cloud.google.com/appengine/docs/python/ndb/). + + +These samples are used on the following documentation page: + +> https://cloud.google.com/appengine/docs/python/ndb/ + + + +Refer to the [App Engine Samples README](../../README.md) for information on how to run and deploy this sample. diff --git a/appengine/standard/ndb/overview/app.yaml b/appengine/standard/ndb/overview/app.yaml new file mode 100644 index 00000000000..5227472ff0c --- /dev/null +++ b/appengine/standard/ndb/overview/app.yaml @@ -0,0 +1,22 @@ +# This file specifies your Python application's runtime configuration +# including URL routing, versions, static file uploads, etc. See +# https://developers.google.com/appengine/docs/python/config/appconfig +# for details. + +version: 1 +runtime: python27 +api_version: 1 +threadsafe: yes + +# Handlers define how to route requests to your application. +handlers: + +# This handler tells app engine how to route requests to a WSGI application. +# The script value is in the format . +# where is a WSGI application object. +- url: .* # This regex directs all routes to main.app + script: main.app + +libraries: +- name: webapp2 + version: "2.5.2" diff --git a/appengine/standard/ndb/overview/favicon.ico b/appengine/standard/ndb/overview/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..23c553a2966ca4cecf146093f33d8114b4f1e368 GIT binary patch literal 8348 zcmeI0du&tJ8Ng3Vwv|ewX4Hu@my&|vCD%DN@CLnxyptBT_%HjatoYr7%N3zGPed#@ckvA><;HWecDU4&2}*2(h%gm&WgCftWj zkQfW;&mXr@0S&(csd+cjaImbXSy=NC<10}z(efGwgi=;B$dt=HkKYCvp-#3KX+qIu zxx$>zcwk+N=c%1^J9)sO44lmS0##~wzU(43f>wW-p_YXwj>s>3{gHI*^oo1lmJ!X*bw{3UBAKRut zUh5Uy4_94IIkdxC^v{_g%FjuEI+f&;9KQ3i+nK7pU_RivFjP_DTl-&gP_qr$nD=M{ z@z;C3#y5Jsh77svGQ4u$ZX)t}=e52{#Xvk;4qIxNh86kR$OhCAie%&e`U{r{ACQZ@ z-)1#sItef{2L za&U;nm+_lo@lbR7vat&^EcECsH)uSnhlht@etsV4LE5B`j#GW*iur~}xpYlTtc;FC{nl|LK^miZ0y3WU>MIM zmc}94VzEFx9?#&?4l+h;ggzvOeCI$ob=`tBp*&EtirH2xU z4a#KB)IMa{_YdYrnxq>-DrrQ>rfpYe8@|Nc-oHnG*L(HR$}d4Eo2&X@o1~9l@%@W) zU{%rv$`tBAOHJH+?zcv7`!T~x;=<#7@4R5T^5$-%Pz-NAYt+5{d;>R+9W24y&`;ZkZqFGQ6_)<35Z_$5V#ga#=N9985-M0KR*fl@h4M0BxWvbYQr6t zULtYBAMJ%iU>x|?U8z_Zyv1jg_VcZ^kO)pd_)jk`_~2MHZmybbloW?l)lnMrb~TAX zV&%#e+VLvM4!%js+%B6}N!=udFlN5Jv;yQm0sdW({6ny+{{$_b`%pI&q3%#p3R?q( z%3@zpafPo4-GA}ErIfU@j?gpfdgeyI);;S-otz(GefQbXvG3J6hbwD_0*YOuqb1V8 zpQm{(oT|g$!tcw;Ck6l>=mo$z0J@0f0^U0{x?+mD8}QB{9yV7qQ;&$5^%*fb@qUB& zX(O<{D-aZ493K;1xH%!}9+}vt8Sshs4os9)d132glTa=lIJv}MJyU_Y!ZFl62j9@l zggg2y{y~c&Vm0cMa@}r@bbn?HR4Sa|66p;nlMl_6D%_%EjCNRq)NBvx!R!t`@zUoW zKV#O#EhZy4>~?VU+rfg@_W_4Kt~zS<|3JjVMcZ#exy;pDUypq|xjpD&0#H{BHgrvM zI=z9nnTN~NCX?mf@DOU;!82-#gXGsw5CSSf22lJ>u8duE&igGuZq4lVZf*LH2%QqOqkv@O{w`Y}q~yWm0EP zeSP~Hau49ZBU@&hWwH5YG0dnGVN1^iztQFh>rIvj5$iQ;q^nyuSuXo`U~{E8DpuIS zA{kR5oC9o=_>jhfRX@QTs1KRm{@F31 zFKP1!w?51@R^I~kA%H*B0W^sKnyVIsv`_2;=zbUGR9pOTqUhV{{^UH=RQ2@S?`uaQ zEy`)Gsd}1IEedW&4lAe0Sg5h`nQXqaZ}RyEzX{FT?hmF3>0_QQT1V~jI$wc&1@aZh ZS0G=3d706zJ{{SaIO|Jj| literal 0 HcmV?d00001 diff --git a/appengine/standard/ndb/overview/index.yaml b/appengine/standard/ndb/overview/index.yaml new file mode 100644 index 00000000000..f933a1c8e6d --- /dev/null +++ b/appengine/standard/ndb/overview/index.yaml @@ -0,0 +1,17 @@ +indexes: + +# AUTOGENERATED + +# This index.yaml is automatically updated whenever the dev_appserver +# detects that a new type of query is run. If you want to manage the +# index.yaml file manually, remove the above marker line (the line +# saying "# AUTOGENERATED"). If you want to manage some indexes +# manually, move them above the marker line. The index.yaml file is +# automatically uploaded to the admin console when you next deploy +# your application using appcfg.py. + +- kind: Greeting + ancestor: yes + properties: + - name: date + direction: desc diff --git a/appengine/standard/ndb/overview/main.py b/appengine/standard/ndb/overview/main.py new file mode 100644 index 00000000000..629d5fae169 --- /dev/null +++ b/appengine/standard/ndb/overview/main.py @@ -0,0 +1,104 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Cloud Datastore NDB API guestbook sample. + +This sample is used on this page: + https://cloud.google.com/appengine/docs/python/ndb/ + +For more information, see README.md +""" + +# [START all] +import cgi +import textwrap +import urllib + +from google.appengine.ext import ndb + +import webapp2 + + +# [START greeting] +class Greeting(ndb.Model): + """Models an individual Guestbook entry with content and date.""" + content = ndb.StringProperty() + date = ndb.DateTimeProperty(auto_now_add=True) +# [END greeting] + +# [START query] + @classmethod + def query_book(cls, ancestor_key): + return cls.query(ancestor=ancestor_key).order(-cls.date) + + +class MainPage(webapp2.RequestHandler): + def get(self): + self.response.out.write('') + guestbook_name = self.request.get('guestbook_name') + ancestor_key = ndb.Key("Book", guestbook_name or "*notitle*") + greetings = Greeting.query_book(ancestor_key).fetch(20) +# [END query] + + greeting_blockquotes = [] + for greeting in greetings: + greeting_blockquotes.append( + '

    %s
    ' % cgi.escape(greeting.content)) + + self.response.out.write(textwrap.dedent("""\ + + + {blockquotes} +
    +
    + +
    +
    + +
    +
    +
    +
    + Guestbook name: + + +
    + + """).format( + blockquotes='\n'.join(greeting_blockquotes), + sign=urllib.urlencode({'guestbook_name': guestbook_name}), + guestbook_name=cgi.escape(guestbook_name))) + + +# [START submit] +class SubmitForm(webapp2.RequestHandler): + def post(self): + # We set the parent key on each 'Greeting' to ensure each guestbook's + # greetings are in the same entity group. + guestbook_name = self.request.get('guestbook_name') + greeting = Greeting(parent=ndb.Key("Book", + guestbook_name or "*notitle*"), + content=self.request.get('content')) + greeting.put() +# [END submit] + self.redirect('/?' + urllib.urlencode( + {'guestbook_name': guestbook_name})) + + +app = webapp2.WSGIApplication([ + ('/', MainPage), + ('/sign', SubmitForm) +]) +# [END all] diff --git a/appengine/standard/ndb/overview/main_test.py b/appengine/standard/ndb/overview/main_test.py new file mode 100644 index 00000000000..1851b78db88 --- /dev/null +++ b/appengine/standard/ndb/overview/main_test.py @@ -0,0 +1,23 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import webtest + +import main + + +def test_app(testbed): + app = webtest.TestApp(main.app) + response = app.get('/') + assert response.status_int == 200 diff --git a/appengine/standard/ndb/projection_queries/README.md b/appengine/standard/ndb/projection_queries/README.md new file mode 100644 index 00000000000..586c2f89efb --- /dev/null +++ b/appengine/standard/ndb/projection_queries/README.md @@ -0,0 +1,11 @@ +## App Engine Datastore NDB Projection Queries Samples + +This contains snippets used in the NDB projection queries documentation, +demonstrating various ways to make ndb projection queries. + + +These samples are used on the following documentation page: + +> https://cloud.google.com/appengine/docs/python/datastore/projectionqueries + + diff --git a/appengine/standard/ndb/projection_queries/snippets.py b/appengine/standard/ndb/projection_queries/snippets.py new file mode 100644 index 00000000000..fb0c3254351 --- /dev/null +++ b/appengine/standard/ndb/projection_queries/snippets.py @@ -0,0 +1,61 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from google.appengine.ext import ndb + + +class Article(ndb.Model): + title = ndb.StringProperty() + author = ndb.StringProperty() + tags = ndb.StringProperty(repeated=True) + + +def print_author_tags(): + query = Article.query() + articles = query.fetch(20, projection=[Article.author, Article.tags]) + for article in articles: + print(article.author) + print(article.tags) + # article.title will raise a ndb.UnprojectedPropertyError + + +class Address(ndb.Model): + type = ndb.StringProperty() # E.g., 'home', 'work' + street = ndb.StringProperty() + city = ndb.StringProperty() + + +class Contact(ndb.Model): + name = ndb.StringProperty() + addresses = ndb.StructuredProperty(Address, repeated=True) + + +def fetch_sub_properties(): + Contact.query().fetch(projection=["name", "addresses.city"]) + Contact.query().fetch(projection=[Contact.name, Contact.addresses.city]) + + +def demonstrate_ndb_grouping(): + Article.query(projection=[Article.author], group_by=[Article.author]) + Article.query(projection=[Article.author], distinct=True) + + +class Foo(ndb.Model): + A = ndb.IntegerProperty(repeated=True) + B = ndb.StringProperty(repeated=True) + + +def declare_multiple_valued_property(): + entity = Foo(A=[1, 1, 2, 3], B=['x', 'y', 'x']) + return entity diff --git a/appengine/standard/ndb/projection_queries/snippets_test.py b/appengine/standard/ndb/projection_queries/snippets_test.py new file mode 100644 index 00000000000..e21a5c414e4 --- /dev/null +++ b/appengine/standard/ndb/projection_queries/snippets_test.py @@ -0,0 +1,46 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import snippets + + +def test_print_author_tags(testbed, capsys): + snippets.Article(title="one", author="Two", tags=["three"]).put() + + snippets.print_author_tags() + + stdout, _ = capsys.readouterr() + assert 'Two' in stdout + assert 'three' in stdout + assert 'one' not in stdout + + +def test_fetch_sub_properties(testbed): + address = snippets.Address(type="home", street="college", city="staten") + address.put() + address2 = snippets.Address(type="home", street="brighton", city="staten") + address2.put() + snippets.Contact(name="one", addresses=[address, address2]).put() + + snippets.fetch_sub_properties() + + +def test_demonstrate_ndb_grouping(testbed): + snippets.Article(title="one", author="Two", tags=["three"]).put() + + snippets.demonstrate_ndb_grouping() + + +def test_declare_multiple_valued_property(testbed): + snippets.declare_multiple_valued_property() diff --git a/appengine/standard/ndb/properties/README.md b/appengine/standard/ndb/properties/README.md new file mode 100644 index 00000000000..86532a55c72 --- /dev/null +++ b/appengine/standard/ndb/properties/README.md @@ -0,0 +1,11 @@ +## App Engine Datastore NDB Properties Samples + +This contains snippets used in the NDB properties documentation, demonstrating +various operations on ndb properties. + + +These samples are used on the following documentation page: + +> https://cloud.google.com/appengine/docs/python/datastore/entity-property-reference + + diff --git a/appengine/standard/ndb/properties/snippets.py b/appengine/standard/ndb/properties/snippets.py new file mode 100644 index 00000000000..9c98ac6701d --- /dev/null +++ b/appengine/standard/ndb/properties/snippets.py @@ -0,0 +1,150 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START notestore_imports] +from google.appengine.ext import ndb +from google.appengine.ext.ndb import msgprop +# [END notestore_imports] +from protorpc import messages + + +class Account(ndb.Model): + username = ndb.StringProperty() + userid = ndb.IntegerProperty() + email = ndb.StringProperty() + + +class Employee(ndb.Model): + full_name = ndb.StringProperty('n') + retirement_age = ndb.IntegerProperty('r') + + +class Article(ndb.Model): + title = ndb.StringProperty() + stars = ndb.IntegerProperty() + tags = ndb.StringProperty(repeated=True) + + +def create_article(): + article = Article( + title='Python versus Ruby', + stars=3, + tags=['python', 'ruby']) + article.put() + return article + + +class Address(ndb.Model): + type = ndb.StringProperty() # E.g., 'home', 'work' + street = ndb.StringProperty() + city = ndb.StringProperty() + + +class Contact(ndb.Model): + name = ndb.StringProperty() + addresses = ndb.StructuredProperty(Address, repeated=True) + + +class ContactWithLocalStructuredProperty(ndb.Model): + name = ndb.StringProperty() + addresses = ndb.LocalStructuredProperty(Address, repeated=True) + + +def create_contact(): + guido = Contact( + name='Guido', + addresses=[ + Address( + type='home', + city='Amsterdam'), + Address( + type='work', + street='Spear St', + city='SF')]) + + guido.put() + return guido + + +def create_contact_with_local_structured_property(): + guido = ContactWithLocalStructuredProperty( + name='Guido', + addresses=[ + Address( + type='home', + city='Amsterdam'), + Address( + type='work', + street='Spear St', + city='SF')]) + + guido.put() + return guido + + +class SomeEntity(ndb.Model): + name = ndb.StringProperty() + name_lower = ndb.ComputedProperty(lambda self: self.name.lower()) + + +def create_some_entity(): + entity = SomeEntity(name='Nick') + entity.put() + return entity + + +class Note(messages.Message): + text = messages.StringField(1, required=True) + when = messages.IntegerField(2) + + +class NoteStore(ndb.Model): + note = msgprop.MessageProperty(Note, indexed_fields=['when']) + name = ndb.StringProperty() + + +def create_note_store(): + my_note = Note(text='Excellent note', when=50) + + ns = NoteStore(note=my_note, name='excellent') + key = ns.put() + + new_notes = NoteStore.query(NoteStore.note.when >= 10).fetch() + return new_notes, key + + +class Notebook(messages.Message): + notes = messages.MessageField(Note, 1, repeated=True) + + +class SignedStorableNotebook(ndb.Model): + author = ndb.StringProperty() + nb = msgprop.MessageProperty( + Notebook, indexed_fields=['notes.text', 'notes.when']) + + +class Color(messages.Enum): + RED = 620 + GREEN = 495 + BLUE = 450 + + +class Part(ndb.Model): + name = ndb.StringProperty() + color = msgprop.EnumProperty(Color, required=True) + + +def print_part(): + p1 = Part(name='foo', color=Color.RED) + print p1.color # prints "RED" diff --git a/appengine/standard/ndb/properties/snippets_test.py b/appengine/standard/ndb/properties/snippets_test.py new file mode 100644 index 00000000000..0ac3e5bb8ba --- /dev/null +++ b/appengine/standard/ndb/properties/snippets_test.py @@ -0,0 +1,106 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import snippets + + +def test_account(testbed): + account = snippets.Account( + username='flan', + userid=123, + email='flan@example.com') + account.put() + + +def test_employee(testbed): + employee = snippets.Employee( + full_name='Hob Gadling', + retirement_age=600) + employee.put() + + +def test_article(testbed): + article = snippets.create_article() + assert article.title == 'Python versus Ruby' + assert article.stars == 3 + assert sorted(article.tags) == sorted(['python', 'ruby']) + + +def test_create_contact(testbed): + guido = snippets.create_contact() + assert guido.name == 'Guido' + addresses = guido.addresses + assert addresses[0].type == 'home' + assert addresses[1].type == 'work' + assert addresses[0].street is None + assert addresses[1].street == 'Spear St' + assert addresses[0].city == 'Amsterdam' + assert addresses[1].city == 'SF' + + +def test_contact_with_local_structured_property(testbed): + guido = snippets.create_contact_with_local_structured_property() + assert guido.name == 'Guido' + addresses = guido.addresses + assert addresses[0].type == 'home' + assert addresses[1].type == 'work' + + +def test_create_some_entity(testbed): + entity = snippets.create_some_entity() + assert entity.name == 'Nick' + assert entity.name_lower == 'nick' + + +def test_computed_property(testbed): + entity = snippets.create_some_entity() + entity.name = 'Nick' + assert entity.name_lower == 'nick' + entity.name = 'Nickie' + assert entity.name_lower == 'nickie' + + +def test_create_note_store(testbed): + note_stores, _ = snippets.create_note_store() + assert len(note_stores) == 1 + assert note_stores[0].name == 'excellent' + assert note_stores[0].name == 'excellent' + assert note_stores[0].note.text == 'Excellent note' + assert note_stores[0].note.when == 50 + + +def test_notebook(testbed): + note1 = snippets.Note( + text='Refused to die.', + when=1389) + note2 = snippets.Note( + text='Printed some things', + when=1489) + note3 = snippets.Note( + text='Learned to use a sword', + when=1589) + + notebook = snippets.Notebook( + notes=[note1, note2, note3]) + stored_notebook = snippets.SignedStorableNotebook( + author='Hob Gadling', + nb=notebook) + + stored_notebook.put() + + +def test_part(testbed, capsys): + snippets.print_part() + stdout, _ = capsys.readouterr() + assert stdout.strip() == 'RED' diff --git a/appengine/standard/ndb/property_subclasses/README.md b/appengine/standard/ndb/property_subclasses/README.md new file mode 100644 index 00000000000..31445c72766 --- /dev/null +++ b/appengine/standard/ndb/property_subclasses/README.md @@ -0,0 +1,11 @@ +## App Engine Datastore NDB Property Subclasses Samples + +This contains snippets used in the NDB property subclasses documentation, +demonstrating various operations on ndb property subclasses. + + +These samples are used on the following documentation page: + +> https://cloud.google.com/appengine/docs/python/ndb/subclassprop + + diff --git a/appengine/standard/ndb/property_subclasses/my_models.py b/appengine/standard/ndb/property_subclasses/my_models.py new file mode 100644 index 00000000000..ef1a6e8bdd7 --- /dev/null +++ b/appengine/standard/ndb/property_subclasses/my_models.py @@ -0,0 +1,106 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from datetime import date + +from google.appengine.ext import ndb + + +class LongIntegerProperty(ndb.StringProperty): + def _validate(self, value): + if not isinstance(value, (int, long)): + raise TypeError('expected an integer, got %s' % repr(value)) + + def _to_base_type(self, value): + return str(value) # Doesn't matter if it's an int or a long + + def _from_base_type(self, value): + return long(value) # Always return a long + + +class BoundedLongIntegerProperty(ndb.StringProperty): + def __init__(self, bits, **kwds): + assert isinstance(bits, int) + assert bits > 0 and bits % 4 == 0 # Make it simple to use hex + super(BoundedLongIntegerProperty, self).__init__(**kwds) + self._bits = bits + + def _validate(self, value): + assert -(2 ** (self._bits - 1)) <= value < 2 ** (self._bits - 1) + + def _to_base_type(self, value): + # convert from signed -> unsigned + if value < 0: + value += 2 ** self._bits + assert 0 <= value < 2 ** self._bits + # Return number as a zero-padded hex string with correct number of + # digits: + return '%0*x' % (self._bits // 4, value) + + def _from_base_type(self, value): + value = int(value, 16) + if value >= 2 ** (self._bits - 1): + value -= 2 ** self._bits + return value + + +# Define an entity class holding some long integers. +class MyModel(ndb.Model): + name = ndb.StringProperty() + abc = LongIntegerProperty(default=0) + xyz = LongIntegerProperty(repeated=True) + + +class FuzzyDate(object): + def __init__(self, first, last=None): + assert isinstance(first, date) + assert last is None or isinstance(last, date) + self.first = first + self.last = last or first + + +class FuzzyDateModel(ndb.Model): + first = ndb.DateProperty() + last = ndb.DateProperty() + + +class FuzzyDateProperty(ndb.StructuredProperty): + def __init__(self, **kwds): + super(FuzzyDateProperty, self).__init__(FuzzyDateModel, **kwds) + + def _validate(self, value): + assert isinstance(value, FuzzyDate) + + def _to_base_type(self, value): + return FuzzyDateModel(first=value.first, last=value.last) + + def _from_base_type(self, value): + return FuzzyDate(value.first, value.last) + + +class MaybeFuzzyDateProperty(FuzzyDateProperty): + def _validate(self, value): + if isinstance(value, date): + return FuzzyDate(value) # Must return the converted value! + # Otherwise, return None and leave validation to the base class + + +# Class to record historic people and events in their life. +class HistoricPerson(ndb.Model): + name = ndb.StringProperty() + birth = FuzzyDateProperty() + death = FuzzyDateProperty() + # Parallel lists: + event_dates = FuzzyDateProperty(repeated=True) + event_names = ndb.StringProperty(repeated=True) diff --git a/appengine/standard/ndb/property_subclasses/snippets.py b/appengine/standard/ndb/property_subclasses/snippets.py new file mode 100644 index 00000000000..50c04a192d5 --- /dev/null +++ b/appengine/standard/ndb/property_subclasses/snippets.py @@ -0,0 +1,57 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from datetime import date + +import my_models + + +def create_entity(): + # Create an entity and write it to the Datastore. + entity = my_models.MyModel(name='booh', xyz=[10**100, 6**666]) + assert entity.abc == 0 + key = entity.put() + return key + + +def read_and_update_entity(key): + # Read an entity back from the Datastore and update it. + entity = key.get() + entity.abc += 1 + entity.xyz.append(entity.abc//3) + entity.put() + + +def query_entity(): + # Query for a MyModel entity whose xyz contains 6**666. + # (NOTE: using ordering operations don't work, but == does.) + results = my_models.MyModel.query( + my_models.MyModel.xyz == 6**666).fetch(10) + return results + + +def create_and_query_columbus(): + columbus = my_models.HistoricPerson( + name='Christopher Columbus', + birth=my_models.FuzzyDate(date(1451, 8, 22), date(1451, 10, 31)), + death=my_models.FuzzyDate(date(1506, 5, 20)), + event_dates=[my_models.FuzzyDate( + date(1492, 1, 1), date(1492, 12, 31))], + event_names=['Discovery of America']) + columbus.put() + + # Query for historic people born no later than 1451. + results = my_models.HistoricPerson.query( + my_models.HistoricPerson.birth.last <= date(1451, 12, 31)).fetch() + return results diff --git a/appengine/standard/ndb/property_subclasses/snippets_test.py b/appengine/standard/ndb/property_subclasses/snippets_test.py new file mode 100644 index 00000000000..8aa621620fa --- /dev/null +++ b/appengine/standard/ndb/property_subclasses/snippets_test.py @@ -0,0 +1,98 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import datetime + +from google.appengine.ext import ndb +import pytest + +import my_models +import snippets + + +def test_create_entity(testbed): + assert my_models.MyModel.query().count() == 0 + snippets.create_entity() + entities = my_models.MyModel.query().fetch() + assert len(entities) == 1 + assert entities[0].name == 'booh' + + +def test_read_and_update_entity(testbed): + key = snippets.create_entity() + entities = my_models.MyModel.query().fetch() + assert len(entities) == 1 + assert entities[0].abc == 0 + len_xyz = len(entities[0].xyz) + + snippets.read_and_update_entity(key) + entities = my_models.MyModel.query().fetch() + assert len(entities) == 1 + assert entities[0].abc == 1 + assert len(entities[0].xyz) == len_xyz + 1 + + +def test_query_entity(testbed): + results = snippets.query_entity() + assert len(results) == 0 + + snippets.create_entity() + results = snippets.query_entity() + assert len(results) == 1 + + +def test_create_columbus(testbed): + entities = snippets.create_and_query_columbus() + assert len(entities) == 1 + assert entities[0].name == 'Christopher Columbus' + assert (entities[0].birth.first < entities[0].birth.last < + entities[0].death.first) + + +def test_long_integer_property(testbed): + with pytest.raises(TypeError): + my_models.MyModel( + name='not integer test', + xyz=['not integer']) + + +def test_bounded_long_integer_property(testbed): + class TestBoundedLongIntegerProperty(ndb.Model): + num = my_models.BoundedLongIntegerProperty(4) + + # Test out of the bounds + with pytest.raises(AssertionError): + TestBoundedLongIntegerProperty(num=0xF) + with pytest.raises(AssertionError): + TestBoundedLongIntegerProperty(num=-0xF) + + # This should work + working_instance = TestBoundedLongIntegerProperty(num=0b111) + assert working_instance.num == 0b111 + working_instance.num = 0b10 + assert working_instance.num == 2 + + +def test_maybe_fuzzy_date_property(testbed): + class TestMaybeFuzzyDateProperty(ndb.Model): + first_date = my_models.MaybeFuzzyDateProperty() + second_date = my_models.MaybeFuzzyDateProperty() + + two_types_of_dates = TestMaybeFuzzyDateProperty( + first_date=my_models.FuzzyDate( + datetime.date(1984, 2, 27), datetime.date(1984, 2, 29)), + second_date=datetime.date(2015, 6, 27)) + + assert isinstance(two_types_of_dates.first_date, my_models.FuzzyDate) + assert isinstance(two_types_of_dates.second_date, my_models.FuzzyDate) diff --git a/appengine/standard/ndb/queries/README.md b/appengine/standard/ndb/queries/README.md new file mode 100644 index 00000000000..067655c6674 --- /dev/null +++ b/appengine/standard/ndb/queries/README.md @@ -0,0 +1,11 @@ +## App Engine Datastore NDB Queries Samples + +This contains snippets used in the NDB queries documentation, demonstrating +various ways to make ndb queries. + + +These samples are used on the following documentation page: + +> https://cloud.google.com/appengine/docs/python/ndb/queries + + diff --git a/appengine/standard/ndb/queries/guestbook.py b/appengine/standard/ndb/queries/guestbook.py new file mode 100644 index 00000000000..b99948f8b54 --- /dev/null +++ b/appengine/standard/ndb/queries/guestbook.py @@ -0,0 +1,75 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import cgi + +from google.appengine.datastore.datastore_query import Cursor +from google.appengine.ext import ndb +import webapp2 + + +class Greeting(ndb.Model): + """Models an individual Guestbook entry with content and date.""" + content = ndb.StringProperty() + date = ndb.DateTimeProperty(auto_now_add=True) + + @classmethod + def query_book(cls, ancestor_key): + return cls.query(ancestor=ancestor_key).order(-cls.date) + + +class MainPage(webapp2.RequestHandler): + GREETINGS_PER_PAGE = 20 + + def get(self): + guestbook_name = self.request.get('guestbook_name') + ancestor_key = ndb.Key('Book', guestbook_name or '*notitle*') + greetings = Greeting.query_book(ancestor_key).fetch( + self.GREETINGS_PER_PAGE) + + self.response.out.write('') + + for greeting in greetings: + self.response.out.write( + '
    %s
    ' % cgi.escape(greeting.content)) + + self.response.out.write('') + + +class List(webapp2.RequestHandler): + GREETINGS_PER_PAGE = 10 + + def get(self): + """Handles requests like /list?cursor=1234567.""" + cursor = Cursor(urlsafe=self.request.get('cursor')) + greets, next_cursor, more = Greeting.query().fetch_page( + self.GREETINGS_PER_PAGE, start_cursor=cursor) + + self.response.out.write('') + + for greeting in greets: + self.response.out.write( + '
    %s
    ' % cgi.escape(greeting.content)) + + if more and next_cursor: + self.response.out.write('More...' % + next_cursor.urlsafe()) + + self.response.out.write('') + + +app = webapp2.WSGIApplication([ + ('/', MainPage), + ('/list', List), +], debug=True) diff --git a/appengine/standard/ndb/queries/guestbook_test.py b/appengine/standard/ndb/queries/guestbook_test.py new file mode 100644 index 00000000000..9ddfcd26902 --- /dev/null +++ b/appengine/standard/ndb/queries/guestbook_test.py @@ -0,0 +1,67 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import re + +from google.appengine.ext import ndb +import pytest +import webtest + +import guestbook + + +@pytest.fixture +def app(testbed): + return webtest.TestApp(guestbook.app) + + +def test_main(app): + # Add a greeting to find + guestbook.Greeting( + content='Hello world', + parent=ndb.Key('Book', 'brane3')).put() + + # Add a greeting to not find. + guestbook.Greeting( + content='Flat sheet', + parent=ndb.Key('Book', 'brane2')).put() + + response = app.get('/?guestbook_name=brane3') + + assert response.status_int == 200 + assert 'Hello world' in response.body + assert 'Flat sheet' not in response.body + + +def test_list(app): + # Add greetings to find + for i in range(11): + guestbook.Greeting(content='Greeting {}'.format(i)).put() + + response = app.get('/list') + assert response.status_int == 200 + + assert 'Greeting 0' in response.body + assert 'Greeting 9' in response.body + assert 'Greeting 10' not in response.body + + next_page = re.search(r'href="([^"]+)"', response.body).group(1) + assert next_page is not None + + response = app.get(next_page) + assert response.status_int == 200 + + assert 'Greeting 0' not in response.body + assert 'Greeting 10' in response.body + assert 'More' not in response.body diff --git a/appengine/standard/ndb/queries/snippets.py b/appengine/standard/ndb/queries/snippets.py new file mode 100644 index 00000000000..987bd8c1c62 --- /dev/null +++ b/appengine/standard/ndb/queries/snippets.py @@ -0,0 +1,236 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from google.appengine.ext import ndb + +from guestbook import Greeting +from snippets_models import (Account, Address, Article, + Bar, Contact, Employee, FlexEmployee, Manager) + + +def query_account_equality(): + query = Account.query(Account.userid == 42) + return query + + +def query_account_inequality(): + query = Account.query(Account.userid >= 40) + return query + + +def query_account_multiple_filters(): + query = Account.query(Account.userid >= 40, Account.userid < 50) + return query + + +def query_account_in_steps(): + query1 = Account.query() # Retrieve all Account entitites + query2 = query1.filter(Account.userid >= 40) # Filter on userid >= 40 + query3 = query2.filter(Account.userid < 50) # Filter on userid < 50 too + return query1, query2, query3 + + +def query_article_inequality(): + query = Article.query(Article.tags != 'perl') + return query + + +def query_article_inequality_explicit(): + query = Article.query(ndb.OR(Article.tags < 'perl', + Article.tags > 'perl')) + return query + + +def articles_with_tags_example(): + # [START included_in_inequality] + Article(title='Perl + Python = Parrot', + stars=5, + tags=['python', 'perl']) + # [END included_in_inequality] + # [START excluded_from_inequality] + Article(title='Introduction to Perl', + stars=3, + tags=['perl']) + # [END excluded_from_inequality] + + +def query_article_in(): + query = Article.query(Article.tags.IN(['python', 'ruby', 'php'])) + return query + + +def query_article_in_equivalent(): + query = Article.query(ndb.OR(Article.tags == 'python', + Article.tags == 'ruby', + Article.tags == 'php')) + return query + + +def query_article_nested(): + query = Article.query(ndb.AND(Article.tags == 'python', + ndb.OR(Article.tags.IN(['ruby', 'jruby']), + ndb.AND(Article.tags == 'php', + Article.tags != 'perl')))) + return query + + +def query_greeting_order(): + query = Greeting.query().order(Greeting.content, -Greeting.date) + return query + + +def query_greeting_multiple_orders(): + query = Greeting.query().order(Greeting.content).order(-Greeting.date) + return query + + +def query_purchase_with_customer_key(): + # [START purchase_with_customer_key_models] + class Customer(ndb.Model): + name = ndb.StringProperty() + + class Purchase(ndb.Model): + customer = ndb.KeyProperty(kind=Customer) + price = ndb.IntegerProperty() + # [END purchase_with_customer_key_models] + + def query_purchases_for_customer_via_key(customer_entity): + purchases = Purchase.query( + Purchase.customer == customer_entity.key).fetch() + return purchases + + return Customer, Purchase, query_purchases_for_customer_via_key + + +def query_purchase_with_ancestor_key(): + # [START purchase_with_ancestor_key_models] + class Customer(ndb.Model): + name = ndb.StringProperty() + + class Purchase(ndb.Model): + price = ndb.IntegerProperty() + # [END purchase_with_ancestor_key_models] + + def create_purchase_for_customer_with_ancestor(customer_entity): + purchase = Purchase(parent=customer_entity.key) + return purchase + + def query_for_purchases_of_customer_with_ancestor(customer_entity): + purchases = Purchase.query(ancestor=customer_entity.key).fetch() + return purchases + + return (Customer, Purchase, + create_purchase_for_customer_with_ancestor, + query_for_purchases_of_customer_with_ancestor) + + +def print_query(): + print(Employee.query()) + # -> Query(kind='Employee') + print(Employee.query(ancestor=ndb.Key(Manager, 1))) + # -> Query(kind='Employee', ancestor=Key('Manager', 1)) + + +def query_contact_with_city(): + query = Contact.query(Contact.addresses.city == 'Amsterdam') + return query + + +def query_contact_sub_entities_beware(): + query = Contact.query(Contact.addresses.city == 'Amsterdam', # Beware! + Contact.addresses.street == 'Spear St') + return query + + +def query_contact_multiple_values_in_single_sub_entity(): + query = Contact.query(Contact.addresses == Address(city='San Francisco', + street='Spear St')) + return query + + +def query_properties_named_by_string_on_expando(): + property_to_query = 'location' + query = FlexEmployee.query(ndb.GenericProperty(property_to_query) == 'SF') + return query + + +def query_properties_named_by_string_for_defined_properties(keyword, value): + query = Article.query(Article._properties[keyword] == value) + return query + + +def query_properties_named_by_string_using_getattr(keyword, value): + query = Article.query(getattr(Article, keyword) == value) + return query + + +def order_query_results_by_property(keyword): + expando_query = FlexEmployee.query().order(ndb.GenericProperty('location')) + + property_query = Article.query().order(Article._properties[keyword]) + + return expando_query, property_query + + +def print_query_keys(query): + for key in query.iter(keys_only=True): + print(key) + + +def reverse_queries(): + # Set up. + q = Bar.query() + q_forward = q.order(Bar.key) + q_reverse = q.order(-Bar.key) + + # Fetch a page going forward. + bars, cursor, more = q_forward.fetch_page(10) + + # Fetch the same page going backward. + r_bars, r_cursor, r_more = q_reverse.fetch_page(10, start_cursor=cursor) + + return (bars, cursor, more), (r_bars, r_cursor, r_more) + + +def fetch_message_accounts_inefficient(message_query): + message_account_pairs = [] + for message in message_query: + key = ndb.Key('Account', message.userid) + account = key.get() + message_account_pairs.append((message, account)) + + return message_account_pairs + + +def fetch_message_accounts_efficient(message_query): + def callback(message): + key = ndb.Key('Account', message.userid) + account = key.get() + return message, account + + message_account_pairs = message_query.map(callback) + # Now message_account_pairs is a list of (message, account) tuples. + return message_account_pairs + + +def fetch_good_articles_using_gql_with_explicit_bind(): + query = ndb.gql("SELECT * FROM Article WHERE stars > :1") + query2 = query.bind(3) + + return query, query2 + + +def fetch_good_articles_using_gql_with_inlined_bind(): + query = ndb.gql("SELECT * FROM Article WHERE stars > :1", 3) + return query diff --git a/appengine/standard/ndb/queries/snippets_models.py b/appengine/standard/ndb/queries/snippets_models.py new file mode 100644 index 00000000000..be497850e34 --- /dev/null +++ b/appengine/standard/ndb/queries/snippets_models.py @@ -0,0 +1,65 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from google.appengine.ext import ndb + + +class Account(ndb.Model): + username = ndb.StringProperty() + userid = ndb.IntegerProperty() + email = ndb.StringProperty() + + +class Address(ndb.Model): + type = ndb.StringProperty() # E.g., 'home', 'work' + street = ndb.StringProperty() + city = ndb.StringProperty() + + +class Contact(ndb.Model): + name = ndb.StringProperty() + addresses = ndb.StructuredProperty(Address, repeated=True) + + +class Article(ndb.Model): + title = ndb.StringProperty() + stars = ndb.IntegerProperty() + tags = ndb.StringProperty(repeated=True) + + +class ArticleWithDifferentDatastoreName(ndb.Model): + title = ndb.StringProperty('t') + + +class Employee(ndb.Model): + full_name = ndb.StringProperty('n') + retirement_age = ndb.IntegerProperty('r') + + +class Manager(ndb.Model): + pass + + +class FlexEmployee(ndb.Expando): + name = ndb.StringProperty() + age = ndb.IntegerProperty() + + +class Bar(ndb.Model): + pass + + +class Message(ndb.Model): + content = ndb.StringProperty() + userid = ndb.IntegerProperty() diff --git a/appengine/standard/ndb/queries/snippets_test.py b/appengine/standard/ndb/queries/snippets_test.py new file mode 100644 index 00000000000..d8e6ddef79c --- /dev/null +++ b/appengine/standard/ndb/queries/snippets_test.py @@ -0,0 +1,388 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from guestbook import Greeting + +import snippets +from snippets_models import (Account, Address, Article, + Bar, Contact, FlexEmployee, Message) + + +def test_query_account_equality(testbed): + Account(userid=42).put() + Account(userid=43).put() + + query = snippets.query_account_equality() + accounts = query.fetch() + + assert len(accounts) == 1 + assert accounts[0].userid == 42 + + +def test_query_account_inequality(testbed): + Account(userid=32).put() + Account(userid=42).put() + Account(userid=43).put() + + query = snippets.query_account_inequality() + accounts = query.fetch() + + assert len(accounts) == 2 + assert all(a.userid > 40 for a in accounts) + + +def test_query_account_multiple_filters(testbed): + Account(userid=40).put() + Account(userid=49).put() + Account(userid=50).put() + Account(userid=6).put() + Account(userid=62).put() + + query = snippets.query_account_multiple_filters() + accounts = query.fetch() + + assert len(accounts) == 2 + assert all(40 <= a.userid < 50 for a in accounts) + + +def test_query_account_in_steps(testbed): + Account(userid=40).put() + Account(userid=49).put() + Account(userid=50).put() + Account(userid=6).put() + Account(userid=62).put() + + _, _, query = snippets.query_account_in_steps() + accounts = query.fetch() + + assert len(accounts) == 2 + assert all(40 <= a.userid < 50 for a in accounts) + + +def test_query_article_inequality(testbed): + Article(tags=['perl']).put() + Article(tags=['perl', 'python']).put() + + query = snippets.query_article_inequality() + articles = query.fetch() + + assert len(articles) == 1 + + +def test_query_article_inequality_explicit(testbed): + Article(tags=['perl']).put() + Article(tags=['perl', 'python']).put() + + query = snippets.query_article_inequality_explicit() + articles = query.fetch() + + assert len(articles) == 1 + + +def test_articles_with_tags_example(testbed): + snippets.articles_with_tags_example() + + +def test_query_article_in(testbed): + Article(tags=['perl']).put() + Article(tags=['perl', 'python']).put() + Article(tags=['ruby']).put() + Article(tags=['php']).put() + + query = snippets.query_article_in() + articles = query.fetch() + + assert len(articles) == 3 + + +def test_query_article_in_equivalent(testbed): + Article(tags=['perl']).put() + Article(tags=['perl', 'python']).put() + Article(tags=['ruby']).put() + Article(tags=['php']).put() + + query = snippets.query_article_in_equivalent() + articles = query.fetch() + + assert len(articles) == 3 + + +def test_query_article_nested(testbed): + Article(tags=['python']).put() # excluded - no non-python + Article(tags=['ruby']).put() # excluded - no python + Article(tags=['python', 'ruby']).put() # included + Article(tags=['python', 'jruby']).put() # included + Article(tags=['python', 'ruby', 'jruby']).put() # included + Article(tags=['python', 'php']).put() # included + Article(tags=['python', 'perl']).put() # excluded + + query = snippets.query_article_nested() + articles = query.fetch() + assert len(articles) == 4 + + +def test_query_greeting_order(testbed): + Greeting(content='3').put() + Greeting(content='2').put() + Greeting(content='1').put() + Greeting(content='2').put() + + query = snippets.query_greeting_order() + greetings = query.fetch() + + assert (greetings[0].content < greetings[1].content < greetings[3].content) + assert greetings[1].content == greetings[2].content + assert greetings[1].date > greetings[2].date + + +def test_query_greeting_multiple_orders(testbed): + Greeting(content='3').put() + Greeting(content='2').put() + Greeting(content='1').put() + Greeting(content='2').put() + + query = snippets.query_greeting_multiple_orders() + greetings = query.fetch() + + assert (greetings[0].content < greetings[1].content < greetings[3].content) + assert greetings[1].content == greetings[2].content + assert greetings[1].date > greetings[2].date + + +def test_query_purchase_with_customer_key(testbed): + Customer, Purchase, do_query = snippets.query_purchase_with_customer_key() + charles = Customer(name='Charles') + charles.put() + snoop_key = Customer(name='Snoop').put() + Purchase(price=123, customer=charles.key).put() + Purchase(price=234, customer=snoop_key).put() + + purchases = do_query(charles) + assert len(purchases) == 1 + assert purchases[0].price == 123 + + +def test_query_purchase_with_ancestor_key(testbed): + Customer, Purchase, create_purchase, do_query = ( + snippets.query_purchase_with_ancestor_key()) + charles = Customer(name='Charles') + charles.put() + snoop = Customer(name='Snoop') + snoop.put() + + charles_purchase = create_purchase(charles) + charles_purchase.price = 123 + charles_purchase.put() + + snoop_purchase = create_purchase(snoop) + snoop_purchase.price = 234 + snoop_purchase.put() + + purchases = do_query(snoop) + assert len(purchases) == 1 + assert purchases[0].price == 234 + + +def test_print_query(testbed, capsys): + snippets.print_query() + stdout, _ = capsys.readouterr() + + assert '' in stdout + + +def test_query_contact_with_city(testbed): + address = Address(type='home', street='Spear St', city='Amsterdam') + address.put() + Contact(name='Bertus Aafjes', addresses=[address]).put() + address1 = Address(type='home', street='Damrak St', city='Amsterdam') + address1.put() + address2 = Address(type='work', street='Spear St', city='San Francisco') + address2.put() + Contact(name='Willem Jan Aalders', addresses=[address1, address2]).put() + address = Address(type='home', street='29th St', city='San Francisco') + address.put() + Contact(name='Hans Aarsman', addresses=[address]).put() + + query = snippets.query_contact_with_city() + contacts = query.fetch() + + assert len(contacts) == 2 + + +def test_query_contact_sub_entities_beware(testbed): + address = Address(type='home', street='Spear St', city='Amsterdam') + address.put() + Contact(name='Bertus Aafjes', addresses=[address]).put() + address1 = Address(type='home', street='Damrak St', city='Amsterdam') + address1.put() + address2 = Address(type='work', street='Spear St', city='San Francisco') + address2.put() + Contact(name='Willem Jan Aalders', addresses=[address1, address2]).put() + address = Address(type='home', street='29th St', city='San Francisco') + address.put() + Contact(name='Hans Aarsman', addresses=[address]).put() + + query = snippets.query_contact_sub_entities_beware() + contacts = query.fetch() + + assert len(contacts) == 2 + for contact in contacts: + assert ('Spear St' in [a.street for a in contact.addresses] or + 'Amsterdam' in [a.city for a in contact.addresses]) + + +def test_query_contact_multiple_values_in_single_sub_entity(testbed): + address = Address(type='home', street='Spear St', city='Amsterdam') + address.put() + Contact(name='Bertus Aafjes', addresses=[address]).put() + address1 = Address(type='home', street='Damrak St', city='Amsterdam') + address1.put() + address2 = Address(type='work', street='Spear St', city='San Francisco') + address2.put() + Contact(name='Willem Jan Aalders', addresses=[address1, address2]).put() + address = Address(type='home', street='29th St', city='San Francisco') + address.put() + Contact(name='Hans Aarsman', addresses=[address]).put() + + query = snippets.query_contact_multiple_values_in_single_sub_entity() + contacts = query.fetch() + + assert len(contacts) == 1 + assert any(a.city == 'San Francisco' and a.street == 'Spear St' + for a in contacts[0].addresses) + + +def test_query_properties_named_by_string_on_expando(testbed): + FlexEmployee(location='SF').put() + FlexEmployee(location='Amsterdam').put() + + query = snippets.query_properties_named_by_string_on_expando() + employees = query.fetch() + assert len(employees) == 1 + + +def test_query_properties_named_by_string_for_defined_properties(testbed): + Article(title='from').put() + Article(title='to').put() + + query = snippets.query_properties_named_by_string_for_defined_properties( + 'title', 'from') + articles = query.fetch() + + assert len(articles) == 1 + + +def test_query_properties_named_by_string_using_getattr(testbed): + Article(title='from').put() + Article(title='to').put() + + query = snippets.query_properties_named_by_string_using_getattr( + 'title', 'from') + articles = query.fetch() + + assert len(articles) == 1 + + +def test_order_query_results_by_property(testbed): + Article(title='2').put() + Article(title='1').put() + FlexEmployee(location=2).put() + FlexEmployee(location=1).put() + expando_query, property_query = snippets.order_query_results_by_property( + 'title') + + assert expando_query.fetch()[0].location == 1 + assert property_query.fetch()[0].title == '1' + + +def test_print_query_keys(testbed, capsys): + for i in range(3): + Article(title='title {}'.format(i)).put() + + snippets.print_query_keys(Article.query()) + + stdout, _ = capsys.readouterr() + assert "Key('Article'" in stdout + + +def test_reverse_queries(testbed): + for i in range(11): + Bar().put() + + (bars, cursor, more), (r_bars, r_cursor, r_more) = ( + snippets.reverse_queries()) + + assert len(bars) == 10 + assert len(r_bars) == 10 + + for prev_bar, bar in zip(bars, bars[1:]): + assert prev_bar.key < bar.key + + for prev_bar, bar in zip(r_bars, r_bars[1:]): + assert prev_bar.key > bar.key + + +def test_fetch_message_accounts_inefficient(testbed): + for i in range(1, 6): + Account(username='Account %s' % i, id=i).put() + Message(content='Message %s' % i, userid=i).put() + + message_account_pairs = snippets.fetch_message_accounts_inefficient( + Message.query().order(Message.userid)) + + assert len(message_account_pairs) == 5 + + print repr(message_account_pairs) + for i in range(1, 6): + message, account = message_account_pairs[i - 1] + assert message.content == 'Message %s' % i + assert account.username == 'Account %s' % i + + +def test_fetch_message_accounts_efficient(testbed): + for i in range(1, 6): + Account(username='Account %s' % i, id=i).put() + Message(content='Message %s' % i, userid=i).put() + + message_account_pairs = snippets.fetch_message_accounts_efficient( + Message.query().order(Message.userid)) + + assert len(message_account_pairs) == 5 + + for i in range(1, 6): + message, account = message_account_pairs[i - 1] + assert message.content == 'Message %s' % i + assert account.username == 'Account %s' % i + + +def test_fetch_good_articles_using_gql_with_explicit_bind(testbed): + for i in range(1, 6): + Article(stars=i).put() + + query, query2 = snippets.fetch_good_articles_using_gql_with_explicit_bind() + articles = query2.fetch() + + assert len(articles) == 2 + assert all(a.stars > 3 for a in articles) + + +def test_fetch_good_articles_using_gql_with_inlined_bind(testbed): + for i in range(1, 6): + Article(stars=i).put() + + query = snippets.fetch_good_articles_using_gql_with_inlined_bind() + articles = query.fetch() + + assert len(articles) == 2 + assert all(a.stars > 3 for a in articles) diff --git a/appengine/standard/ndb/schema_update/app.yaml b/appengine/standard/ndb/schema_update/app.yaml new file mode 100644 index 00000000000..f20b30eb39e --- /dev/null +++ b/appengine/standard/ndb/schema_update/app.yaml @@ -0,0 +1,17 @@ +runtime: python27 +api_version: 1 +threadsafe: true + +builtins: +# Deferred is required to use google.appengine.ext.deferred. +- deferred: on + +handlers: +- url: /.* + script: main.app + +libraries: +- name: webapp2 + version: "2.5.2" +- name: jinja2 + version: "2.6" diff --git a/appengine/standard/ndb/schema_update/main.py b/appengine/standard/ndb/schema_update/main.py new file mode 100644 index 00000000000..60071850539 --- /dev/null +++ b/appengine/standard/ndb/schema_update/main.py @@ -0,0 +1,139 @@ +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Sample application that shows how to perform a "schema migration" using +Google Cloud Datastore. + +This application uses one model named "Pictures" but two different versions +of it. v2 contains two extra fields. The application shows how to +populate these new fields onto entities that existed prior to adding the +new fields to the model class. +""" +# [START imports] +import logging +import os + +from google.appengine.ext import deferred +from google.appengine.ext import ndb +import jinja2 +import webapp2 + +import models_v1 +import models_v2 + + +JINJA_ENVIRONMENT = jinja2.Environment( + loader=jinja2.FileSystemLoader( + os.path.join(os.path.dirname(__file__), 'templates')), + extensions=['jinja2.ext.autoescape'], + autoescape=True) +# [END imports] + + +# [START display_entities] +class DisplayEntitiesHandler(webapp2.RequestHandler): + """Displays the current set of entities and options to add entities + or update the schema.""" + def get(self): + # Force ndb to use v2 of the model by re-loading it. + reload(models_v2) + + entities = models_v2.Picture.query().fetch() + template_values = { + 'entities': entities, + } + + template = JINJA_ENVIRONMENT.get_template('index.html') + self.response.write(template.render(template_values)) +# [END display_entities] + + +# [START add_entities] +class AddEntitiesHandler(webapp2.RequestHandler): + """Adds new entities using the v1 schema.""" + def post(self): + # Force ndb to use v1 of the model by re-loading it. + reload(models_v1) + + # Save some example data. + ndb.put_multi([ + models_v1.Picture(author='Alice', name='Sunset'), + models_v1.Picture(author='Bob', name='Sunrise') + ]) + + self.response.write(""" + Entities created. View entities. + """) +# [END add_entities] + + +# [START update_schema] +class UpdateSchemaHandler(webapp2.RequestHandler): + """Queues a task to start updating the model schema.""" + def post(self): + deferred.defer(update_schema_task) + self.response.write(""" + Schema update started. Check the console for task progress. + View entities. + """) + + +def update_schema_task(cursor=None, num_updated=0, batch_size=100): + """Task that handles updating the models' schema. + + This is started by + UpdateSchemaHandler. It scans every entity in the datastore for the + Picture model and re-saves it so that it has the new schema fields. + """ + + # Force ndb to use v2 of the model by re-loading it. + reload(models_v2) + + # Get all of the entities for this Model. + query = models_v2.Picture.query() + pictures, next_cursor, more = query.fetch_page( + batch_size, start_cursor=cursor) + + to_put = [] + for picture in pictures: + # Give the new fields default values. + # If you added new fields and were okay with the default values, you + # would not need to do this. + picture.num_votes = 1 + picture.avg_rating = 5 + to_put.append(picture) + + # Save the updated entities. + if to_put: + ndb.put_multi(to_put) + num_updated += len(to_put) + logging.info( + 'Put {} entities to Datastore for a total of {}'.format( + len(to_put), num_updated)) + + # If there are more entities, re-queue this task for the next page. + if more: + deferred.defer( + update_schema_task, cursor=next_cursor, num_updated=num_updated) + else: + logging.debug( + 'update_schema_task complete with {0} updates!'.format( + num_updated)) +# [END update_schema] + + +app = webapp2.WSGIApplication([ + ('/', DisplayEntitiesHandler), + ('/add_entities', AddEntitiesHandler), + ('/update_schema', UpdateSchemaHandler)]) diff --git a/appengine/standard/ndb/schema_update/main_test.py b/appengine/standard/ndb/schema_update/main_test.py new file mode 100644 index 00000000000..eb4a0aa15a3 --- /dev/null +++ b/appengine/standard/ndb/schema_update/main_test.py @@ -0,0 +1,62 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from google.appengine.ext import deferred +import pytest +import webtest + +import main +import models_v1 +import models_v2 + + +@pytest.fixture +def app(testbed): + yield webtest.TestApp(main.app) + + +def test_app(app): + response = app.get('/') + assert response.status_int == 200 + + +def test_add_entities(app): + response = app.post('/add_entities') + assert response.status_int == 200 + response = app.get('/') + assert response.status_int == 200 + assert 'Author: Bob' in response.body + assert 'Name: Sunrise' in response.body + assert 'Author: Alice' in response.body + assert 'Name: Sunset' in response.body + + +def test_update_schema(app, testbed): + reload(models_v1) + test_model = models_v1.Picture(author='Test', name='Test') + test_model.put() + + response = app.post('/update_schema') + assert response.status_int == 200 + + # Run the queued task. + tasks = testbed.taskqueue_stub.get_filtered_tasks() + assert len(tasks) == 1 + deferred.run(tasks[0].payload) + + # Check the updated items + reload(models_v2) + updated_model = test_model.key.get() + assert updated_model.num_votes == 1 + assert updated_model.avg_rating == 5.0 diff --git a/appengine/standard/ndb/schema_update/models_v1.py b/appengine/standard/ndb/schema_update/models_v1.py new file mode 100644 index 00000000000..ea84bc9b869 --- /dev/null +++ b/appengine/standard/ndb/schema_update/models_v1.py @@ -0,0 +1,20 @@ +# Copyright 2015 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from google.appengine.ext import ndb + + +class Picture(ndb.Model): + author = ndb.StringProperty() + name = ndb.StringProperty(default='') diff --git a/appengine/standard/ndb/schema_update/models_v2.py b/appengine/standard/ndb/schema_update/models_v2.py new file mode 100644 index 00000000000..8f2c79ff3c6 --- /dev/null +++ b/appengine/standard/ndb/schema_update/models_v2.py @@ -0,0 +1,23 @@ +# Copyright 2015 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from google.appengine.ext import ndb + + +class Picture(ndb.Model): + author = ndb.StringProperty() + name = ndb.StringProperty(default='') + # Two new fields + num_votes = ndb.IntegerProperty(default=0) + avg_rating = ndb.FloatProperty(default=0) diff --git a/appengine/standard/ndb/schema_update/templates/index.html b/appengine/standard/ndb/schema_update/templates/index.html new file mode 100644 index 00000000000..cc3b7ca8782 --- /dev/null +++ b/appengine/standard/ndb/schema_update/templates/index.html @@ -0,0 +1,47 @@ +{# +# Copyright 2015 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#} + + + + + +

    If you've just added or updated entities, you may need to refresh + the page to see the changes due to + eventual consitency.

    + {% for entity in entities %} +

    + Author: {{entity.author}}, + Name: {{entity.name}}, + {% if 'num_votes' in entity._values %} + Votes: {{entity.num_votes}}, + {% endif %} + {% if 'avg_rating' in entity._values %} + Average Rating: {{entity.avg_rating}} + {% endif %} +

    + {% endfor %} + {% if entities|length == 0 %} +
    + +
    + {% endif %} + {% if entities|length > 0 %} +
    + +
    + {% endif %} + + diff --git a/appengine/standard/ndb/transactions/README.md b/appengine/standard/ndb/transactions/README.md new file mode 100644 index 00000000000..78dc33ab269 --- /dev/null +++ b/appengine/standard/ndb/transactions/README.md @@ -0,0 +1,14 @@ +## App Engine Datastore NDB Transactions Sample + +This is a sample app for Google App Engine that demonstrates the [NDB Transactions Python API](https://cloud.google.com/appengine/docs/python/ndb/transactions) + +This app presents a list of notes. After you submit a note with a particular title, you may not change that note or submit a new note with the same title. There are multiple note pages available. + + +These samples are used on the following documentation page: + +> https://cloud.google.com/appengine/docs/python/ndb/transactions + + + +Refer to the [App Engine Samples README](../../README.md) for information on how to run and deploy this sample. diff --git a/appengine/standard/ndb/transactions/app.yaml b/appengine/standard/ndb/transactions/app.yaml new file mode 100644 index 00000000000..a71a3a86c34 --- /dev/null +++ b/appengine/standard/ndb/transactions/app.yaml @@ -0,0 +1,18 @@ +# This file specifies your Python application's runtime configuration +# including URL routing, versions, static file uploads, etc. See +# https://developers.google.com/appengine/docs/python/config/appconfig +# for details. + +version: 1 +runtime: python27 +api_version: 1 +threadsafe: yes + +# Handlers define how to route requests to your application. +handlers: + +# This handler tells app engine how to route requests to a WSGI application. +# The script value is in the format . +# where is a WSGI application object. +- url: .* # This regex directs all routes to main.app + script: main.app diff --git a/appengine/standard/ndb/transactions/appengine_config.py b/appengine/standard/ndb/transactions/appengine_config.py new file mode 100644 index 00000000000..a51535059b2 --- /dev/null +++ b/appengine/standard/ndb/transactions/appengine_config.py @@ -0,0 +1,11 @@ +""" +`appengine_config.py` is automatically loaded when Google App Engine +starts a new instance of your application. This runs before any +WSGI applications specified in app.yaml are loaded. +""" + +from google.appengine.ext import vendor + +# Third-party libraries are stored in "lib", vendoring will make +# sure that they are importable by the application. +vendor.add('lib') diff --git a/appengine/standard/ndb/transactions/favicon.ico b/appengine/standard/ndb/transactions/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..23c553a2966ca4cecf146093f33d8114b4f1e368 GIT binary patch literal 8348 zcmeI0du&tJ8Ng3Vwv|ewX4Hu@my&|vCD%DN@CLnxyptBT_%HjatoYr7%N3zGPed#@ckvA><;HWecDU4&2}*2(h%gm&WgCftWj zkQfW;&mXr@0S&(csd+cjaImbXSy=NC<10}z(efGwgi=;B$dt=HkKYCvp-#3KX+qIu zxx$>zcwk+N=c%1^J9)sO44lmS0##~wzU(43f>wW-p_YXwj>s>3{gHI*^oo1lmJ!X*bw{3UBAKRut zUh5Uy4_94IIkdxC^v{_g%FjuEI+f&;9KQ3i+nK7pU_RivFjP_DTl-&gP_qr$nD=M{ z@z;C3#y5Jsh77svGQ4u$ZX)t}=e52{#Xvk;4qIxNh86kR$OhCAie%&e`U{r{ACQZ@ z-)1#sItef{2L za&U;nm+_lo@lbR7vat&^EcECsH)uSnhlht@etsV4LE5B`j#GW*iur~}xpYlTtc;FC{nl|LK^miZ0y3WU>MIM zmc}94VzEFx9?#&?4l+h;ggzvOeCI$ob=`tBp*&EtirH2xU z4a#KB)IMa{_YdYrnxq>-DrrQ>rfpYe8@|Nc-oHnG*L(HR$}d4Eo2&X@o1~9l@%@W) zU{%rv$`tBAOHJH+?zcv7`!T~x;=<#7@4R5T^5$-%Pz-NAYt+5{d;>R+9W24y&`;ZkZqFGQ6_)<35Z_$5V#ga#=N9985-M0KR*fl@h4M0BxWvbYQr6t zULtYBAMJ%iU>x|?U8z_Zyv1jg_VcZ^kO)pd_)jk`_~2MHZmybbloW?l)lnMrb~TAX zV&%#e+VLvM4!%js+%B6}N!=udFlN5Jv;yQm0sdW({6ny+{{$_b`%pI&q3%#p3R?q( z%3@zpafPo4-GA}ErIfU@j?gpfdgeyI);;S-otz(GefQbXvG3J6hbwD_0*YOuqb1V8 zpQm{(oT|g$!tcw;Ck6l>=mo$z0J@0f0^U0{x?+mD8}QB{9yV7qQ;&$5^%*fb@qUB& zX(O<{D-aZ493K;1xH%!}9+}vt8Sshs4os9)d132glTa=lIJv}MJyU_Y!ZFl62j9@l zggg2y{y~c&Vm0cMa@}r@bbn?HR4Sa|66p;nlMl_6D%_%EjCNRq)NBvx!R!t`@zUoW zKV#O#EhZy4>~?VU+rfg@_W_4Kt~zS<|3JjVMcZ#exy;pDUypq|xjpD&0#H{BHgrvM zI=z9nnTN~NCX?mf@DOU;!82-#gXGsw5CSSf22lJ>u8duE&igGuZq4lVZf*LH2%QqOqkv@O{w`Y}q~yWm0EP zeSP~Hau49ZBU@&hWwH5YG0dnGVN1^iztQFh>rIvj5$iQ;q^nyuSuXo`U~{E8DpuIS zA{kR5oC9o=_>jhfRX@QTs1KRm{@F31 zFKP1!w?51@R^I~kA%H*B0W^sKnyVIsv`_2;=zbUGR9pOTqUhV{{^UH=RQ2@S?`uaQ zEy`)Gsd}1IEedW&4lAe0Sg5h`nQXqaZ}RyEzX{FT?hmF3>0_QQT1V~jI$wc&1@aZh ZS0G=3d706zJ{{SaIO|Jj| literal 0 HcmV?d00001 diff --git a/appengine/standard/ndb/transactions/main.py b/appengine/standard/ndb/transactions/main.py new file mode 100644 index 00000000000..503fada8459 --- /dev/null +++ b/appengine/standard/ndb/transactions/main.py @@ -0,0 +1,193 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import cgi +import random +import urllib + +import flask + +# [START taskq-imp] +from google.appengine.api import taskqueue +from google.appengine.ext import ndb +# [END taskq-imp] + + +class Note(ndb.Model): + """Models an individual Note entry with content.""" + content = ndb.StringProperty() + + +def parent_key(page_name): + return ndb.Key("Parent", page_name) + + +app = flask.Flask(__name__) + + +@app.route('/') +def main_page(): + page_name = flask.request.args.get('page_name', 'default') + response = """ + +

    Permanent note page: %s

    """ % cgi.escape(page_name) + + parent = parent_key(page_name) + notes = Note.query(ancestor=parent).fetch(20) + for note in notes: + response += '

    %s

    ' % cgi.escape(note.key.id()) + response += '
    %s
    ' % cgi.escape(note.content) + + response += ( + """
    +
    + Submit Note:
    + +
    """ + % urllib.urlencode({'page_name': page_name})) + response += """ +
    +
    Switch page: +
    + + """ % cgi.escape(page_name, quote=True) + + return response + + +# [START standard] +@ndb.transactional +def insert_if_absent(note_key, note): + fetch = note_key.get() + if fetch is None: + note.put() + return True + return False +# [END standard] + + +# [START two-tries] +@ndb.transactional(retries=1) +def insert_if_absent_2_retries(note_key, note): + # do insert + # [END two-tries] + fetch = note_key.get() + if fetch is None: + note.put() + return True + return False + + +# [START cross-group] +@ndb.transactional(xg=True) +def insert_if_absent_xg(note_key, note): + # do insert + # [END cross-group] + fetch = note_key.get() + if fetch is None: + note.put() + return True + return False + + +# [START sometimes] +def insert_if_absent_sometimes(note_key, note): + # do insert + # [END sometimes] + fetch = note_key.get() + if fetch is None: + note.put() + return True + return False + + +# [START indep] +@ndb.transactional(propagation=ndb.TransactionOptions.INDEPENDENT) +def insert_if_absent_indep(note_key, note): + # do insert + # [END indep] + fetch = note_key.get() + if fetch is None: + note.put() + return True + return False + + +# [START taskq] +@ndb.transactional +def insert_if_absent_taskq(note_key, note): + taskqueue.add(url=flask.url_for('taskq_worker'), transactional=True) + # do insert + # [END taskq] + fetch = note_key.get() + if fetch is None: + note.put() + return True + return False + + +@app.route('/worker') +def taskq_worker(): + pass + + +def pick_random_insert(note_key, note): + choice = random.randint(0, 5) + if choice == 0: + # [START calling2] + inserted = insert_if_absent(note_key, note) + # [END calling2] + elif choice == 1: + inserted = insert_if_absent_2_retries(note_key, note) + elif choice == 2: + inserted = insert_if_absent_xg(note_key, note) + elif choice == 3: + # [START sometimes-call] + inserted = ndb.transaction(lambda: + insert_if_absent_sometimes(note_key, note)) + # [END sometimes-call] + elif choice == 4: + inserted = insert_if_absent_indep(note_key, note) + elif choice == 5: + inserted = insert_if_absent_taskq(note_key, note) + return inserted + + +@app.route('/add', methods=['POST']) +def add_note(): + page_name = flask.request.args.get('page_name', 'default') + note_title = flask.request.form['note_title'] + note_text = flask.request.form['note_text'] + + parent = parent_key(page_name) + + choice = random.randint(0, 1) + if choice == 0: + # Use transactional function + # [START calling] + note_key = ndb.Key(Note, note_title, parent=parent) + note = Note(key=note_key, content=note_text) + # [END calling] + if pick_random_insert(note_key, note) is False: + return ('Already there
    Return' + % flask.url_for('main_page', page_name=page_name)) + return flask.redirect(flask.url_for('main_page', page_name=page_name)) + elif choice == 1: + # Use get_or_insert, which is transactional + note = Note.get_or_insert(note_title, parent=parent, content=note_text) + if note.content != note_text: + return ('Already there
    Return' + % flask.url_for('main_page', page_name=page_name)) + return flask.redirect(flask.url_for('main_page', page_name=page_name)) diff --git a/appengine/standard/ndb/transactions/main_test.py b/appengine/standard/ndb/transactions/main_test.py new file mode 100644 index 00000000000..cd19af5bffc --- /dev/null +++ b/appengine/standard/ndb/transactions/main_test.py @@ -0,0 +1,50 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +import main + + +@pytest.fixture +def app(testbed): + main.app.config['TESTING'] = True + return main.app.test_client() + + +def test_index(app): + rv = app.get('/') + assert 'Permanent note page' in rv.data + assert rv.status == '200 OK' + + +def test_post(app): + rv = app.post('/add', data=dict( + note_title='Title', + note_text='Text' + ), follow_redirects=True) + assert rv.status == '200 OK' + + +def test_there(app): + rv = app.post('/add', data=dict( + note_title='Title', + note_text='New' + ), follow_redirects=True) + rv = app.post('/add', data=dict( + note_title='Title', + note_text='There' + ), follow_redirects=True) + assert 'Already there' in rv.data + assert rv.status == '200 OK' diff --git a/appengine/standard/ndb/transactions/requirements.txt b/appengine/standard/ndb/transactions/requirements.txt new file mode 100644 index 00000000000..52ab1a39019 --- /dev/null +++ b/appengine/standard/ndb/transactions/requirements.txt @@ -0,0 +1 @@ +Flask==0.12.2 diff --git a/appengine/standard/remote_api/app.yaml b/appengine/standard/remote_api/app.yaml new file mode 100644 index 00000000000..fd511db7564 --- /dev/null +++ b/appengine/standard/remote_api/app.yaml @@ -0,0 +1,6 @@ +runtime: python27 +api_version: 1 +threadsafe: true + +builtins: +- remote_api: on diff --git a/appengine/standard/remote_api/client.py b/appengine/standard/remote_api/client.py new file mode 100644 index 00000000000..e8463e15cb9 --- /dev/null +++ b/appengine/standard/remote_api/client.py @@ -0,0 +1,54 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Sample app that uses the Google App Engine Remote API to make calls to the +live App Engine APIs.""" + +# [START all] + +import argparse + +try: + import dev_appserver + dev_appserver.fix_sys_path() +except ImportError: + print('Please make sure the App Engine SDK is in your PYTHONPATH.') + raise + +from google.appengine.ext import ndb +from google.appengine.ext.remote_api import remote_api_stub + + +def main(project_id): + remote_api_stub.ConfigureRemoteApiForOAuth( + '{}.appspot.com'.format(project_id), + '/_ah/remote_api') + + # List the first 10 keys in the datastore. + keys = ndb.Query().fetch(10, keys_only=True) + + for key in keys: + print(key) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument('project_id', help='Your Project ID.') + + args = parser.parse_args() + + main(args.project_id) +# [END all] diff --git a/appengine/standard/requests/README.md b/appengine/standard/requests/README.md new file mode 100644 index 00000000000..406f912e01c --- /dev/null +++ b/appengine/standard/requests/README.md @@ -0,0 +1,10 @@ +## App Engine Requests Docs Snippets + +These snippets demonstrate various aspects of App Engine Python request handling. + + +These samples are used on the following documentation page: + +> https://cloud.google.com/appengine/docs/python/how-requests-are-handled + + diff --git a/appengine/standard/requests/app.yaml b/appengine/standard/requests/app.yaml new file mode 100644 index 00000000000..bdd5bf04dc2 --- /dev/null +++ b/appengine/standard/requests/app.yaml @@ -0,0 +1,7 @@ +runtime: python27 +api_version: 1 +threadsafe: yes + +handlers: +- url: .* # This regex directs all routes to main.app + script: main.app diff --git a/appengine/standard/requests/main.py b/appengine/standard/requests/main.py new file mode 100644 index 00000000000..5064b5b937e --- /dev/null +++ b/appengine/standard/requests/main.py @@ -0,0 +1,66 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Sample application that demonstrates various aspects of App Engine's request +handling. +""" + +import os +import time + +import webapp2 + + +# [START request_timer] +class TimerHandler(webapp2.RequestHandler): + def get(self): + from google.appengine.runtime import DeadlineExceededError + + try: + time.sleep(70) + self.response.write('Completed.') + except DeadlineExceededError: + self.response.clear() + self.response.set_status(500) + self.response.out.write( + 'The request did not complete in time.') +# [END request_timer] + + +# [START environment] +class PrintEnvironmentHandler(webapp2.RequestHandler): + def get(self): + self.response.headers['Content-Type'] = 'text/plain' + for key, value in os.environ.iteritems(): + self.response.out.write( + "{} = {}\n".format(key, value)) +# [END environment] + + +# [START request_ids] +class RequestIdHandler(webapp2.RequestHandler): + def get(self): + self.response.headers['Content-Type'] = 'text/plain' + request_id = os.environ.get('REQUEST_LOG_ID') + self.response.write( + 'REQUEST_LOG_ID={}'.format(request_id)) +# [END request_ids] + + +app = webapp2.WSGIApplication([ + ('/timer', TimerHandler), + ('/environment', PrintEnvironmentHandler), + ('/requestid', RequestIdHandler) +], debug=True) diff --git a/appengine/standard/requests/main_test.py b/appengine/standard/requests/main_test.py new file mode 100644 index 00000000000..c978856824f --- /dev/null +++ b/appengine/standard/requests/main_test.py @@ -0,0 +1,45 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +from google.appengine.runtime import DeadlineExceededError +import mock +import webtest + +import main + + +def test_timer(testbed): + app = webtest.TestApp(main.app) + + with mock.patch('main.time.sleep') as sleep_mock: + sleep_mock.side_effect = DeadlineExceededError() + app.get('/timer', status=500) + assert sleep_mock.called + + +def test_environment(testbed): + app = webtest.TestApp(main.app) + response = app.get('/environment') + assert response.headers['Content-Type'] == 'text/plain' + assert response.body + + +def test_request_id(testbed): + app = webtest.TestApp(main.app) + os.environ['REQUEST_LOG_ID'] = '1234' + response = app.get('/requestid') + assert response.headers['Content-Type'] == 'text/plain' + assert '1234' in response.body diff --git a/appengine/standard/resources/app.yaml b/appengine/standard/resources/app.yaml new file mode 100644 index 00000000000..18650e00fe6 --- /dev/null +++ b/appengine/standard/resources/app.yaml @@ -0,0 +1,11 @@ +# dummy app.yaml for tests + +api_version: 1 +runtime: python27 +threadsafe: true + +handlers: +- url: / + static_files: README.md + upload: README.md + mime_type: text/plain diff --git a/appengine/standard/search/snippets/snippets.py b/appengine/standard/search/snippets/snippets.py new file mode 100644 index 00000000000..de4d373ad26 --- /dev/null +++ b/appengine/standard/search/snippets/snippets.py @@ -0,0 +1,287 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from datetime import datetime + +from google.appengine.api import search + + +def simple_search(index): + index.search('rose water') + + +def search_date(index): + index.search('1776-07-04') + + +def search_terms(index): + # search for documents with pianos that cost less than $5000 + index.search("product = piano AND price < 5000") + + +def create_document(): + document = search.Document( + # Setting the doc_id is optional. If omitted, the search service will + # create an identifier. + doc_id='PA6-5000', + fields=[ + search.TextField(name='customer', value='Joe Jackson'), + search.HtmlField( + name='comment', value='this is marked up text'), + search.NumberField(name='number_of_visits', value=7), + search.DateField(name='last_visit', value=datetime.now()), + search.DateField( + name='birthday', value=datetime(year=1960, month=6, day=19)), + search.GeoField( + name='home_location', value=search.GeoPoint(37.619, -122.37)) + ]) + return document + + +def add_document_to_index(document): + index = search.Index('products') + index.put(document) + + +def add_document_and_get_doc_id(documents): + index = search.Index('products') + results = index.put(documents) + document_ids = [document.id for document in results] + return document_ids + + +def get_document_by_id(): + index = search.Index('products') + + # Get a single document by ID. + document = index.get("AZ125") + + # Get a range of documents starting with a given ID. + documents = index.get_range(start_id="AZ125", limit=100) + + return document, documents + + +def query_index(): + index = search.Index('products') + query_string = 'product: piano AND price < 5000' + + results = index.search(query_string) + + for scored_document in results: + print(scored_document) + + +def delete_all_in_index(index): + # index.get_range by returns up to 100 documents at a time, so we must + # loop until we've deleted all items. + while True: + # Use ids_only to get the list of document IDs in the index without + # the overhead of getting the entire document. + document_ids = [ + document.doc_id + for document + in index.get_range(ids_only=True)] + + # If no IDs were returned, we've deleted everything. + if not document_ids: + break + + # Delete the documents for the given IDs + index.delete(document_ids) + + +def async_query(index): + futures = [index.search_async('foo'), index.search_async('bar')] + results = [future.get_result() for future in futures] + return results + + +def query_options(): + index = search.Index('products') + query_string = "product: piano AND price < 5000" + + # Create sort options to sort on price and brand. + sort_price = search.SortExpression( + expression='price', + direction=search.SortExpression.DESCENDING, + default_value=0) + sort_brand = search.SortExpression( + expression='brand', + direction=search.SortExpression.DESCENDING, + default_value="") + sort_options = search.SortOptions(expressions=[sort_price, sort_brand]) + + # Create field expressions to add new fields to the scored documents. + price_per_note_expression = search.FieldExpression( + name='price_per_note', expression='price/88') + ivory_expression = search.FieldExpression( + name='ivory', expression='snippet("ivory", summary, 120)') + + # Create query options using the sort options and expressions created + # above. + query_options = search.QueryOptions( + limit=25, + returned_fields=['model', 'price', 'description'], + returned_expressions=[price_per_note_expression, ivory_expression], + sort_options=sort_options) + + # Build the Query and run the search + query = search.Query(query_string=query_string, options=query_options) + results = index.search(query) + for scored_document in results: + print(scored_document) + + +def query_results(index, query_string): + result = index.search(query_string) + total_matches = result.number_found + list_of_docs = result.results + number_of_docs_returned = len(list_of_docs) + return total_matches, list_of_docs, number_of_docs_returned + + +def query_offset(index, query_string): + offset = 0 + + while True: + # Build the query using the current offset. + options = search.QueryOptions(offset=offset) + query = search.Query(query_string=query_string, options=options) + + # Get the results + results = index.search(query) + + number_retrieved = len(results.results) + if number_retrieved == 0: + break + + # Add the number of documents found to the offset, so that the next + # iteration will grab the next page of documents. + offset += number_retrieved + + # Process the matched documents + for document in results: + print(document) + + +def query_cursor(index, query_string): + cursor = search.Cursor() + + while cursor: + # Build the query using the cursor. + options = search.QueryOptions(cursor=cursor) + query = search.Query(query_string=query_string, options=options) + + # Get the results and the next cursor + results = index.search(query) + cursor = results.cursor + + for document in results: + print(document) + + +def query_per_document_cursor(index, query_string): + cursor = search.Cursor(per_result=True) + + # Build the query using the cursor. + options = search.QueryOptions(cursor=cursor) + query = search.Query(query_string=query_string, options=options) + + # Get the results. + results = index.search(query) + + document_cursor = None + for document in results: + # discover some document of interest and grab its cursor, for this + # sample we'll just use the first document. + document_cursor = document.cursor + break + + # Start the next search from the document of interest. + if document_cursor is None: + return + + options = search.QueryOptions(cursor=document_cursor) + query = search.Query(query_string=query_string, options=options) + results = index.search(query) + + for document in results: + print(document) + + +def saving_and_restoring_cursor(cursor): + # Convert the cursor to a web-safe string. + cursor_string = cursor.web_safe_string + # Restore the cursor from a web-safe string. + cursor = search.Cursor(web_safe_string=cursor_string) + + +def add_faceted_document(index): + document = search.Document( + doc_id='doc1', + fields=[ + search.AtomField(name='name', value='x86')], + facets=[ + search.AtomFacet(name='type', value='computer'), + search.NumberFacet(name='ram_size_gb', value=8)]) + + index.put(document) + + +def facet_discovery(index): + # Create the query and enable facet discovery. + query = search.Query('name:x86', enable_facet_discovery=True) + results = index.search(query) + + for facet in results.facets: + print('facet {}.'.format(facet.name)) + for value in facet.values: + print('{}: count={}, refinement_token={}'.format( + value.label, value.count, value.refinement_token)) + + +def facet_by_name(index): + # Create the query and specify to only return the "type" and "ram_size_gb" + # facets. + query = search.Query('name:x86', return_facets=['type', 'ram_size_gb']) + results = index.search(query) + + for facet in results.facets: + print('facet {}'.format(facet.name)) + for value in facet.values: + print('{}: count={}, refinement_token={}'.format( + value.label, value.count, value.refinement_token)) + + +def facet_by_name_and_value(index): + # Create the query and specify to return the "type" facet with values + # "computer" and "printer" and the "ram_size_gb" facet with value in the + # ranges [0,4), [4, 8), and [8, max]. + query = search.Query( + 'name:x86', + return_facets=[ + search.FacetRequest('type', values=['computer', 'printer']), + search.FacetRequest('ram_size_gb', ranges=[ + search.FacetRange(end=4), + search.FacetRange(start=4, end=8), + search.FacetRange(start=8)]) + ]) + + results = index.search(query) + for facet in results.facets: + print('facet {}'.format(facet.name)) + for value in facet.values: + print('{}: count={}, refinement_token={}'.format( + value.label, value.count, value.refinement_token)) diff --git a/appengine/standard/search/snippets/snippets_test.py b/appengine/standard/search/snippets/snippets_test.py new file mode 100644 index 00000000000..3d22bbb5561 --- /dev/null +++ b/appengine/standard/search/snippets/snippets_test.py @@ -0,0 +1,140 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from google.appengine.api import search +import pytest + +import snippets + + +@pytest.fixture +def search_stub(testbed): + testbed.init_search_stub() + + +@pytest.fixture +def index(search_stub): + return search.Index('products') + + +@pytest.fixture +def document(): + return search.Document( + doc_id='doc1', + fields=[ + search.TextField(name='title', value='Meep: A biography')]) + + +def test_simple_search(index): + snippets.simple_search(index) + + +def test_search_date(index): + snippets.search_date(index) + + +def test_search_terms(index): + snippets.search_terms(index) + + +def test_create_document(): + assert snippets.create_document() + + +def test_add_document_to_index(index, document): + snippets.add_document_to_index(document) + assert index.get(document.doc_id) + + +def test_add_document_and_get_doc_id(index, document): + ids = snippets.add_document_and_get_doc_id([document]) + assert ids == [document.doc_id] + + +def test_get_document_by_id(index): + index.put(search.Document(doc_id='AZ124')) + index.put(search.Document(doc_id='AZ125')) + index.put(search.Document(doc_id='AZ126')) + + doc, docs = snippets.get_document_by_id() + + assert doc.doc_id == 'AZ125' + assert [x.doc_id for x in docs] == ['AZ125', 'AZ126'] + + +def test_query_index(index): + snippets.query_index() + + +def test_delete_all_in_index(index, document): + index.put(document) + snippets.delete_all_in_index(index) + assert not index.get(document.doc_id) + + +def test_async_query(index): + snippets.async_query(index) + + +def test_query_options(index): + snippets.query_options() + + +def test_query_results(index, document): + index.put(document) + total_matches, list_of_docs, number_of_docs_returned = ( + snippets.query_results(index, 'meep')) + + assert total_matches == 1 + assert list_of_docs + assert number_of_docs_returned == 1 + + +def test_query_offset(index, document): + index.put(document) + snippets.query_offset(index, 'meep') + + +def test_query_cursor(index, document): + index.put(document) + snippets.query_cursor(index, 'meep') + + +def test_query_per_document_cursor(index, document): + index.put(document) + snippets.query_per_document_cursor(index, 'meep') + + +def test_saving_and_restoring_cursor(index): + snippets.saving_and_restoring_cursor(search.Cursor()) + + +def test_add_faceted_document(index): + snippets.add_faceted_document(index) + + +def test_facet_discovery(index): + snippets.add_faceted_document(index) + snippets.facet_discovery(index) + + +def test_facet_by_name(index): + snippets.add_faceted_document(index) + snippets.facet_by_name(index) + + +def test_facet_by_name_and_value(index): + snippets.add_faceted_document(index) + snippets.facet_by_name_and_value(index) diff --git a/appengine/standard/sendgrid/README.md b/appengine/standard/sendgrid/README.md new file mode 100644 index 00000000000..fc4b596d290 --- /dev/null +++ b/appengine/standard/sendgrid/README.md @@ -0,0 +1,13 @@ +# Sendgrid & Google App Engine + +This sample application demonstrates how to use [Sendgrid with Google App Engine](https://cloud.google.com/appengine/docs/python/mail/sendgrid) + +Refer to the [App Engine Samples README](../../README.md) for information on how to run and deploy this sample. + +# Setup + +Before running this sample: + +1. You will need a [Sendgrid account](http://sendgrid.com/partner/google). +2. Update the `SENGRID_DOMAIN_NAME` and `SENGRID_API_KEY` constants in `main.py`. You can use +the [Sendgrid sandbox domain](https://support.sendgrid.com/hc/en-us/articles/201995663-Safely-Test-Your-Sending-Speed). diff --git a/appengine/standard/sendgrid/app.yaml b/appengine/standard/sendgrid/app.yaml new file mode 100644 index 00000000000..42ad35ed2a8 --- /dev/null +++ b/appengine/standard/sendgrid/app.yaml @@ -0,0 +1,7 @@ +runtime: python27 +threadsafe: yes +api_version: 1 + +handlers: +- url: .* + script: main.app diff --git a/appengine/standard/sendgrid/appengine_config.py b/appengine/standard/sendgrid/appengine_config.py new file mode 100644 index 00000000000..c903d9a0ac5 --- /dev/null +++ b/appengine/standard/sendgrid/appengine_config.py @@ -0,0 +1,18 @@ +# Copyright 2016 Google Inc. +# +# 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.appengine.ext import vendor + +# Add any libraries installed in the "lib" folder. +vendor.add('lib') diff --git a/appengine/standard/sendgrid/main.py b/appengine/standard/sendgrid/main.py new file mode 100644 index 00000000000..1191b3e75cc --- /dev/null +++ b/appengine/standard/sendgrid/main.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python + +# Copyright 2016 Google Inc. +# +# 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 sendgrid-imp] +import sendgrid +from sendgrid.helpers import mail +# [END sendgrid-imp] +import webapp2 + +# make a secure connection to SendGrid +# [START sendgrid-config] +SENDGRID_API_KEY = 'your-sendgrid-api-key' +SENDGRID_SENDER = 'your-sendgrid-sender' +# [END sendgrid-config] + + +def send_simple_message(recipient): + # [START sendgrid-send] + + sg = sendgrid.SendGridAPIClient(apikey=SENDGRID_API_KEY) + + to_email = mail.Email(recipient) + from_email = mail.Email(SENDGRID_SENDER) + subject = 'This is a test email' + content = mail.Content('text/plain', 'Example message.') + message = mail.Mail(from_email, subject, to_email, content) + + response = sg.client.mail.send.post(request_body=message.get()) + + return response + # [END sendgrid-send] + + +class MainPage(webapp2.RequestHandler): + def get(self): + self.response.content_type = 'text/html' + self.response.write(""" + + +
    + + +
    + +""") + + +class SendEmailHandler(webapp2.RequestHandler): + def post(self): + recipient = self.request.get('recipient') + sg_response = send_simple_message(recipient) + self.response.set_status(sg_response.status_code) + self.response.write(sg_response.body) + + +app = webapp2.WSGIApplication([ + ('/', MainPage), + ('/send', SendEmailHandler) +], debug=True) diff --git a/appengine/standard/sendgrid/main_test.py b/appengine/standard/sendgrid/main_test.py new file mode 100644 index 00000000000..72c128fd71a --- /dev/null +++ b/appengine/standard/sendgrid/main_test.py @@ -0,0 +1,46 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import mock +import pytest +import webtest + +import main + + +@pytest.fixture +def app(): + return webtest.TestApp(main.app) + + +def test_get(app): + response = app.get('/') + assert response.status_int == 200 + + +@mock.patch('python_http_client.client.Client._make_request') +def test_post(make_request_mock, app): + response = mock.Mock() + response.getcode.return_value = 200 + response.read.return_value = 'OK' + response.info.return_value = {} + make_request_mock.return_value = response + + app.post('/send', { + 'recipient': 'user@example.com' + }) + + assert make_request_mock.called + request = make_request_mock.call_args[0][1] + assert 'user@example.com' in request.data diff --git a/appengine/standard/sendgrid/requirements.txt b/appengine/standard/sendgrid/requirements.txt new file mode 100644 index 00000000000..db9b503112d --- /dev/null +++ b/appengine/standard/sendgrid/requirements.txt @@ -0,0 +1 @@ +sendgrid==4.2.0 diff --git a/appengine/standard/storage/.gitignore b/appengine/standard/storage/.gitignore new file mode 100644 index 00000000000..a65b41774ad --- /dev/null +++ b/appengine/standard/storage/.gitignore @@ -0,0 +1 @@ +lib diff --git a/appengine/standard/storage/api-client/README.md b/appengine/standard/storage/api-client/README.md new file mode 100644 index 00000000000..b934b2f46ab --- /dev/null +++ b/appengine/standard/storage/api-client/README.md @@ -0,0 +1,15 @@ +# Cloud Storage & Google App Engine + +This sample demonstrates how to use the [Google Cloud Storage API](https://cloud.google.com/storage/docs/json_api/) from Google App Engine. + +Refer to the [App Engine Samples README](../README.md) for information on how to run and deploy this sample. + +## Setup + +Before running the sample: + +1. You need a Cloud Storage Bucket. You create one with [`gsutil`](https://cloud.google.com/storage/docs/gsutil): + + gsutil mb gs://your-bucket-name + +2. Update `main.py` and replace `` with your Cloud Storage bucket. diff --git a/appengine/standard/storage/api-client/app.yaml b/appengine/standard/storage/api-client/app.yaml new file mode 100644 index 00000000000..42ad35ed2a8 --- /dev/null +++ b/appengine/standard/storage/api-client/app.yaml @@ -0,0 +1,7 @@ +runtime: python27 +threadsafe: yes +api_version: 1 + +handlers: +- url: .* + script: main.app diff --git a/appengine/standard/storage/api-client/appengine_config.py b/appengine/standard/storage/api-client/appengine_config.py new file mode 100644 index 00000000000..4fdc5d2b60f --- /dev/null +++ b/appengine/standard/storage/api-client/appengine_config.py @@ -0,0 +1,4 @@ +from google.appengine.ext import vendor + +# Add any libraries installed in the "lib" folder. +vendor.add('lib') diff --git a/appengine/standard/storage/api-client/main.py b/appengine/standard/storage/api-client/main.py new file mode 100644 index 00000000000..bb053ed1198 --- /dev/null +++ b/appengine/standard/storage/api-client/main.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python + +# Copyright 2015 Google Inc. +# +# 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. + +""" +Sample Google App Engine application that lists the objects in a Google Cloud +Storage bucket. + +For more information about Cloud Storage, see README.md in /storage. +For more information about Google App Engine, see README.md in /appengine. +""" + +import json +import StringIO + +import googleapiclient.discovery +import googleapiclient.http +import webapp2 + + +# The bucket that will be used to list objects. +BUCKET_NAME = '' + +storage = googleapiclient.discovery.build('storage', 'v1') + + +class MainPage(webapp2.RequestHandler): + def upload_object(self, bucket, file_object): + body = { + 'name': 'storage-api-client-sample-file.txt', + } + req = storage.objects().insert( + bucket=bucket, body=body, + media_body=googleapiclient.http.MediaIoBaseUpload( + file_object, 'application/octet-stream')) + resp = req.execute() + return resp + + def delete_object(self, bucket, filename): + req = storage.objects().delete(bucket=bucket, object=filename) + resp = req.execute() + return resp + + def get(self): + string_io_file = StringIO.StringIO('Hello World!') + self.upload_object(BUCKET_NAME, string_io_file) + + response = storage.objects().list(bucket=BUCKET_NAME).execute() + self.response.write( + '

    Objects.list raw response:

    ' + '
    {}
    '.format( + json.dumps(response, sort_keys=True, indent=2))) + + self.delete_object(BUCKET_NAME, 'storage-api-client-sample-file.txt') + + +app = webapp2.WSGIApplication([ + ('/', MainPage) +], debug=True) diff --git a/appengine/standard/storage/api-client/main_test.py b/appengine/standard/storage/api-client/main_test.py new file mode 100644 index 00000000000..1fc960987f6 --- /dev/null +++ b/appengine/standard/storage/api-client/main_test.py @@ -0,0 +1,34 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import re + +import webtest + +import main + +PROJECT = os.environ['GCLOUD_PROJECT'] + + +def test_get(): + main.BUCKET_NAME = PROJECT + app = webtest.TestApp(main.app) + + response = app.get('/') + + assert response.status_int == 200 + assert re.search( + re.compile(r'.*.*items.*etag.*', re.DOTALL), + response.body) diff --git a/appengine/standard/storage/api-client/requirements.txt b/appengine/standard/storage/api-client/requirements.txt new file mode 100644 index 00000000000..4f77d6936d7 --- /dev/null +++ b/appengine/standard/storage/api-client/requirements.txt @@ -0,0 +1 @@ +google-api-python-client==1.6.2 diff --git a/appengine/standard/storage/appengine-client/__init__.py b/appengine/standard/storage/appengine-client/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/appengine/standard/storage/appengine-client/app.yaml b/appengine/standard/storage/appengine-client/app.yaml new file mode 100644 index 00000000000..3ec099ad09c --- /dev/null +++ b/appengine/standard/storage/appengine-client/app.yaml @@ -0,0 +1,12 @@ +runtime: python27 +api_version: 1 +threadsafe: yes + +env_variables: + +handlers: +- url: /blobstore.* + script: blobstore.app + +- url: /.* + script: main.app diff --git a/appengine/standard/storage/appengine-client/appengine_config.py b/appengine/standard/storage/appengine-client/appengine_config.py new file mode 100644 index 00000000000..4fdc5d2b60f --- /dev/null +++ b/appengine/standard/storage/appengine-client/appengine_config.py @@ -0,0 +1,4 @@ +from google.appengine.ext import vendor + +# Add any libraries installed in the "lib" folder. +vendor.add('lib') diff --git a/appengine/standard/storage/appengine-client/main.py b/appengine/standard/storage/appengine-client/main.py new file mode 100644 index 00000000000..87ca21501b1 --- /dev/null +++ b/appengine/standard/storage/appengine-client/main.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python + +# Copyright 2017 Google Inc. +# +# 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 sample] +"""A sample app that uses GCS client to operate on bucket and file.""" + +# [START imports] +import os + +import cloudstorage +from google.appengine.api import app_identity + +import webapp2 + +# [END imports] + +# [START retries] +cloudstorage.set_default_retry_params( + cloudstorage.RetryParams( + initial_delay=0.2, max_delay=5.0, backoff_factor=2, max_retry_period=15 + )) +# [END retries] + + +class MainPage(webapp2.RequestHandler): + """Main page for GCS demo application.""" + +# [START get_default_bucket] + def get(self): + bucket_name = os.environ.get( + 'BUCKET_NAME', app_identity.get_default_gcs_bucket_name()) + + self.response.headers['Content-Type'] = 'text/plain' + self.response.write( + 'Demo GCS Application running from Version: {}\n'.format( + os.environ['CURRENT_VERSION_ID'])) + self.response.write('Using bucket name: \n\n'.format(bucket_name)) +# [END get_default_bucket] + + bucket = '/' + bucket_name + filename = bucket + '/demo-testfile' + self.tmp_filenames_to_clean_up = [] + + self.create_file(filename) + self.response.write('\n\n') + + self.read_file(filename) + self.response.write('\n\n') + + self.stat_file(filename) + self.response.write('\n\n') + + self.create_files_for_list_bucket(bucket) + self.response.write('\n\n') + + self.list_bucket(bucket) + self.response.write('\n\n') + + self.list_bucket_directory_mode(bucket) + self.response.write('\n\n') + + self.delete_files() + self.response.write('\n\nThe demo ran successfully!\n') + +# [START write] + def create_file(self, filename): + """Create a file.""" + + self.response.write('Creating file {}\n'.format(filename)) + + # The retry_params specified in the open call will override the default + # retry params for this particular file handle. + write_retry_params = cloudstorage.RetryParams(backoff_factor=1.1) + with cloudstorage.open( + filename, 'w', content_type='text/plain', options={ + 'x-goog-meta-foo': 'foo', 'x-goog-meta-bar': 'bar'}, + retry_params=write_retry_params) as cloudstorage_file: + cloudstorage_file.write('abcde\n') + cloudstorage_file.write('f'*1024*4 + '\n') + self.tmp_filenames_to_clean_up.append(filename) +# [END write] + +# [START read] + def read_file(self, filename): + self.response.write( + 'Abbreviated file content (first line and last 1K):\n') + + with cloudstorage.open(filename) as cloudstorage_file: + self.response.write(cloudstorage_file.readline()) + cloudstorage_file.seek(-1024, os.SEEK_END) + self.response.write(cloudstorage_file.read()) +# [END read] + + def stat_file(self, filename): + self.response.write('File stat:\n') + + stat = cloudstorage.stat(filename) + self.response.write(repr(stat)) + + def create_files_for_list_bucket(self, bucket): + self.response.write('Creating more files for listbucket...\n') + filenames = [bucket + n for n in [ + '/foo1', '/foo2', '/bar', '/bar/1', '/bar/2', '/boo/']] + for f in filenames: + self.create_file(f) + +# [START list_bucket] + def list_bucket(self, bucket): + """Create several files and paginate through them.""" + + self.response.write('Listbucket result:\n') + + # Production apps should set page_size to a practical value. + page_size = 1 + stats = cloudstorage.listbucket(bucket + '/foo', max_keys=page_size) + while True: + count = 0 + for stat in stats: + count += 1 + self.response.write(repr(stat)) + self.response.write('\n') + + if count != page_size or count == 0: + break + stats = cloudstorage.listbucket( + bucket + '/foo', max_keys=page_size, marker=stat.filename) +# [END list_bucket] + + def list_bucket_directory_mode(self, bucket): + self.response.write('Listbucket directory mode result:\n') + for stat in cloudstorage.listbucket(bucket + '/b', delimiter='/'): + self.response.write(stat) + self.response.write('\n') + if stat.is_dir: + for subdir_file in cloudstorage.listbucket( + stat.filename, delimiter='/'): + self.response.write(' {}'.format(subdir_file)) + self.response.write('\n') + +# [START delete_files] + def delete_files(self): + self.response.write('Deleting files...\n') + for filename in self.tmp_filenames_to_clean_up: + self.response.write('Deleting file {}\n'.format(filename)) + try: + cloudstorage.delete(filename) + except cloudstorage.NotFoundError: + pass +# [END delete_files] + + +app = webapp2.WSGIApplication( + [('/', MainPage)], debug=True) +# [END sample] diff --git a/appengine/standard/storage/appengine-client/main_test.py b/appengine/standard/storage/appengine-client/main_test.py new file mode 100644 index 00000000000..c3a05cecc57 --- /dev/null +++ b/appengine/standard/storage/appengine-client/main_test.py @@ -0,0 +1,31 @@ +# Copyright 2017 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +import webtest + +import main + +PROJECT = os.environ['GCLOUD_PROJECT'] + + +def test_get(testbed): + main.BUCKET_NAME = PROJECT + app = webtest.TestApp(main.app) + + response = app.get('/') + + assert response.status_int == 200 + assert 'The demo ran successfully!' in response.body diff --git a/appengine/standard/storage/appengine-client/requirements.txt b/appengine/standard/storage/appengine-client/requirements.txt new file mode 100644 index 00000000000..f2ec35f05f9 --- /dev/null +++ b/appengine/standard/storage/appengine-client/requirements.txt @@ -0,0 +1 @@ +GoogleAppEngineCloudStorageClient==1.9.22.1 diff --git a/appengine/standard/taskqueue/counter/README.md b/appengine/standard/taskqueue/counter/README.md new file mode 100644 index 00000000000..a0eceab788e --- /dev/null +++ b/appengine/standard/taskqueue/counter/README.md @@ -0,0 +1,18 @@ +# App Engine Task Queue Counter + +To run this app locally, specify both `.yaml` files to `dev_appserver.py`: + + dev_appserver.py -A your-app-id app.yaml worker.yaml + +To deploy this application, specify both `.yaml` files to `appcfg.py`: + + appcfg.py update -A your-app-id -V 1 app.yaml worker.yaml + + +These samples are used on the following documentation pages: + +> +* https://cloud.google.com/appengine/docs/python/taskqueue/push/creating-handlers +* https://cloud.google.com/appengine/docs/python/taskqueue/push/creating-tasks + + diff --git a/appengine/standard/taskqueue/counter/app.yaml b/appengine/standard/taskqueue/counter/app.yaml new file mode 100644 index 00000000000..b4e5019bd3c --- /dev/null +++ b/appengine/standard/taskqueue/counter/app.yaml @@ -0,0 +1,8 @@ +runtime: python27 +api_version: 1 +threadsafe: true +module: default + +handlers: +- url: /.* + script: application.app diff --git a/appengine/standard/taskqueue/counter/application.py b/appengine/standard/taskqueue/counter/application.py new file mode 100644 index 00000000000..d4e82891be1 --- /dev/null +++ b/appengine/standard/taskqueue/counter/application.py @@ -0,0 +1,82 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from google.appengine.api import taskqueue +from google.appengine.ext import ndb +import webapp2 + + +COUNTER_KEY = 'default counter' + + +class Counter(ndb.Model): + count = ndb.IntegerProperty(indexed=False) + + +class MainPageHandler(webapp2.RequestHandler): + def get(self): + counter = Counter.get_by_id(COUNTER_KEY) + count = counter.count if counter else 0 + + self.response.write(""" + Count: {count}
    +
    + + + +
    + """.format(count=count)) + + +class EnqueueTaskHandler(webapp2.RequestHandler): + def post(self): + amount = int(self.request.get('amount')) + + task = taskqueue.add( + url='/update_counter', + target='worker', + params={'amount': amount}) + + self.response.write( + 'Task {} enqueued, ETA {}.'.format(task.name, task.eta)) + + +# AsyncEnqueueTaskHandler behaves the same as EnqueueTaskHandler, but shows +# how to queue the task using the asyncronous API. This is not wired up by +# default. To use this, change the MainPageHandler's form action to +# /enqueue_async +class AsyncEnqueueTaskHandler(webapp2.RequestHandler): + def post(self): + amount = int(self.request.get('amount')) + + queue = taskqueue.Queue(name='default') + task = taskqueue.Task( + url='/update_counter', + target='worker', + params={'amount': amount}) + + rpc = queue.add_async(task) + + # Wait for the rpc to complete and return the queued task. + task = rpc.get_result() + + self.response.write( + 'Task {} enqueued, ETA {}.'.format(task.name, task.eta)) + + +app = webapp2.WSGIApplication([ + ('/', MainPageHandler), + ('/enqueue', EnqueueTaskHandler), + ('/enqueue_async', AsyncEnqueueTaskHandler) +], debug=True) diff --git a/appengine/standard/taskqueue/counter/application_test.py b/appengine/standard/taskqueue/counter/application_test.py new file mode 100644 index 00000000000..09a19abbd72 --- /dev/null +++ b/appengine/standard/taskqueue/counter/application_test.py @@ -0,0 +1,32 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import webtest + +import application +import worker + + +def test_all(testbed, run_tasks): + test_app = webtest.TestApp(application.app) + test_worker = webtest.TestApp(worker.app) + + response = test_app.get('/') + assert '0' in response.body + + test_app.post('/enqueue', {'amount': 5}) + run_tasks(test_worker) + + response = test_app.get('/') + assert '5' in response.body diff --git a/appengine/standard/taskqueue/counter/queue.yaml b/appengine/standard/taskqueue/counter/queue.yaml new file mode 100644 index 00000000000..6b677c17a2f --- /dev/null +++ b/appengine/standard/taskqueue/counter/queue.yaml @@ -0,0 +1,4 @@ +queue: +# Change the refresh rate of the default queue from 5/s to 1/s. +- name: default + rate: 1/s diff --git a/appengine/standard/taskqueue/counter/worker.py b/appengine/standard/taskqueue/counter/worker.py new file mode 100644 index 00000000000..21e560892c4 --- /dev/null +++ b/appengine/standard/taskqueue/counter/worker.py @@ -0,0 +1,46 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START all] + +from google.appengine.ext import ndb +import webapp2 + + +COUNTER_KEY = 'default counter' + + +class Counter(ndb.Model): + count = ndb.IntegerProperty(indexed=False) + + +class UpdateCounterHandler(webapp2.RequestHandler): + def post(self): + amount = int(self.request.get('amount')) + + # This task should run at most once per second because of the datastore + # transaction write throughput. + @ndb.transactional + def update_counter(): + counter = Counter.get_or_insert(COUNTER_KEY, count=0) + counter.count += amount + counter.put() + + update_counter() + + +app = webapp2.WSGIApplication([ + ('/update_counter', UpdateCounterHandler) +], debug=True) +# [END all] diff --git a/appengine/standard/taskqueue/counter/worker.yaml b/appengine/standard/taskqueue/counter/worker.yaml new file mode 100644 index 00000000000..da5c95ef161 --- /dev/null +++ b/appengine/standard/taskqueue/counter/worker.yaml @@ -0,0 +1,9 @@ +runtime: python27 +api_version: 1 +threadsafe: true +module: worker + +handlers: +- url: /.* + script: worker.app + login: admin diff --git a/appengine/standard/taskqueue/pull-counter/README.md b/appengine/standard/taskqueue/pull-counter/README.md new file mode 100644 index 00000000000..ac634800a72 --- /dev/null +++ b/appengine/standard/taskqueue/pull-counter/README.md @@ -0,0 +1,8 @@ +# App Engine Task Queue Pull Counter + + +These samples are used on the following documentation page: + +> https://cloud.google.com/appengine/docs/python/taskqueue/overview-pull + + diff --git a/appengine/standard/taskqueue/pull-counter/app.yaml b/appengine/standard/taskqueue/pull-counter/app.yaml new file mode 100644 index 00000000000..e2b5e55709c --- /dev/null +++ b/appengine/standard/taskqueue/pull-counter/app.yaml @@ -0,0 +1,11 @@ +runtime: python27 +api_version: 1 +threadsafe: true + +handlers: +- url: /.* + script: main.app + +libraries: +- name: jinja2 + version: 2.6 diff --git a/appengine/standard/taskqueue/pull-counter/counter.html b/appengine/standard/taskqueue/pull-counter/counter.html new file mode 100644 index 00000000000..a9f6a39d360 --- /dev/null +++ b/appengine/standard/taskqueue/pull-counter/counter.html @@ -0,0 +1,15 @@ + + + +
    + + +
    +