Skip to content

Commit 79555e6

Browse files
Feature: add bundled services samples from jinglundong (GoogleCloudPlatform#8070)
* Feature: add bundled services samples from jinglundong * Feature: texted bundled email * add debugging * Alternate command version * Flask mail app and tests * Flask Mail example complete * Django mail sample complete * WSGI version of Mail sample * Flask Blobstore sample finished * Django Blobstore sample done * WSGI Blobstore sample done * Deferred Flask sample done * Feature: app engine bundled services samples * Fix: address review comments * Temporary debug helper * Fixed debug typo * Fix: Use standard test configurations for each sample * Fix: copyright date * Fix: nox config * Fix: assign version numbers * Fix: blobstore and deferred passing * Fix: log permissions * Fix: show failures for debugging * Fix: run command variation * Fix: turn off type hint check for old ported samples * Fix: fix lint error * Fix: remove whitespace * Fix: more wiggle room for deferred timing Co-authored-by: Maciej Strzelczyk <[email protected]>
1 parent 884d81d commit 79555e6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

75 files changed

+2571
-0
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Access bundled Blobstore services API in Python 3
2+
3+
This folder contains functionally identical apps that allow users to upload
4+
and download files to and from the App Engine Blobstore.
5+
6+
The apps are written using three different web frameworks:
7+
[Flask](https://palletsprojects.com/p/flask/),
8+
[Django](https://www.djangoproject.com/), and the [App Engine bundled services
9+
wsgi functionality](https://github.com/GoogleCloudPlatform/appengine-python-standard).
10+
11+
When run in App Engine, a GET request to the home page (`/` path) will display
12+
a page with a form for selecting a local file and submitting it. When the
13+
form is submitted, the app will save the file in Blobstore and then redirect
14+
the browser to a page that fetches and displays the file.
15+
16+
Deploy this app to App Engine via `gcloud app deploy`. More about App Engine
17+
Bundled Blobstore API at:
18+
https://cloud.google.com/appengine/docs/standard/python3/services/blobstore#python-3-flask
19+
20+
## Testing
21+
22+
The App Engine bundled services can be used from within properly configured App
23+
Engine applications, so testing these apps requires deploying them to App Engine.
24+
25+
Each version of the app includes a test program that will:
26+
27+
1. Launch a new version of the app to App Engine without routing network
28+
requests to it.
29+
1. Interact with the launched app via web requests to the URL of the
30+
specific version that was launched.
31+
1. Delete the newly launched app version. This is possible because it did not
32+
have requests routed to it.
33+
34+
Since each test uses a separate version and addresses test messages and web
35+
requests to that version, multiple tests can run simultaneously without
36+
interfering with each other.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# This file specifies files that are *not* uploaded to Google Cloud Platform
2+
# using gcloud. It follows the same syntax as .gitignore, with the addition of
3+
# "#!include" directives (which insert the entries of the given .gitignore-style
4+
# file at that point).
5+
#
6+
# For more information, run:
7+
# $ gcloud topic gcloudignore
8+
#
9+
.gcloudignore
10+
# If you would like to upload your .git directory, .gitignore file or files
11+
# from your .gitignore file, remove the corresponding line
12+
# below:
13+
.git
14+
.gitignore
15+
16+
# Python pycache:
17+
__pycache__/
18+
# Ignored by the build system
19+
/setup.cfg
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Django app to access bundled Blobstore services API in Python 3
2+
3+
See the [main README](../README.md) for the bundled Blobstore services examples
4+
for information.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Copyright 2022 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
runtime: python39
16+
app_engine_apis: true
17+
18+
handlers:
19+
- url: .*
20+
script: auto
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# Copyright 2022 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from django.conf import settings
16+
from django.core.wsgi import get_wsgi_application
17+
from django.http import HttpResponse
18+
from django.shortcuts import redirect
19+
from django.urls import path
20+
from google.appengine.api import wrap_wsgi_app
21+
from google.appengine.ext import blobstore
22+
from google.appengine.ext import ndb
23+
from google.cloud import logging
24+
25+
# Logging client in Python 3
26+
logging_client = logging.Client()
27+
28+
# This log can be found in the Cloud Logging console under 'Custom Logs'.
29+
logger = logging_client.logger("django-app-logs")
30+
31+
32+
# This datastore model keeps track of which users uploaded which photos.
33+
class UserPhoto(ndb.Model):
34+
blob_key = ndb.BlobKeyProperty()
35+
36+
37+
class PhotoUploadHandler(blobstore.BlobstoreUploadHandler):
38+
def post(self, environ):
39+
upload = self.get_uploads(environ)[0]
40+
photo_key = upload.key()
41+
user_photo = UserPhoto(blob_key=photo_key)
42+
user_photo.put()
43+
logger.log_text("Photo key: %s" % photo_key)
44+
return redirect("view_photo", key=photo_key)
45+
46+
47+
class ViewPhotoHandler(blobstore.BlobstoreDownloadHandler):
48+
def get(self, environ, photo_key):
49+
if not blobstore.get(photo_key):
50+
return HttpResponse("Photo key not found", status=404)
51+
else:
52+
response = HttpResponse(headers=self.send_blob(environ, photo_key))
53+
54+
# Prevent Django from setting a default content-type.
55+
# GAE sets it to a guessed type if the header is not set.
56+
response["Content-Type"] = None
57+
return response
58+
59+
60+
def upload_form(request):
61+
"""Create the HTML form to upload a file."""
62+
upload_url = blobstore.create_upload_url("/upload_photo")
63+
64+
response = """
65+
<html><body>
66+
<form action="{0}" method="POST" enctype="multipart/form-data">
67+
Upload File: <input type="file" name="file"><br>
68+
<input type="submit" name="submit" value="Submit">
69+
</form>
70+
</body></html>""".format(
71+
upload_url
72+
)
73+
74+
return HttpResponse(response)
75+
76+
77+
def view_photo(request, key):
78+
"""View photo given a key."""
79+
return ViewPhotoHandler().get(request.environ, key)
80+
81+
82+
def upload_photo(request):
83+
"""Upload handler called by blobstore when a blob is uploaded in the test."""
84+
return PhotoUploadHandler().post(request.environ)
85+
86+
87+
urlpatterns = (
88+
path("", upload_form, name="upload_form"),
89+
path("view_photo/<key>", view_photo, name="view_photo"),
90+
path("upload_photo", upload_photo, name="upload_photo"),
91+
)
92+
93+
settings.configure(
94+
DEBUG=True,
95+
SECRET_KEY="thisisthesecretkey",
96+
ROOT_URLCONF=__name__,
97+
MIDDLEWARE_CLASSES=(
98+
"django.middleware.common.CommonMiddleware",
99+
"django.middleware.csrf.CsrfViewMiddleware",
100+
"django.middleware.clickjacking.XFrameOptionsMiddleware",
101+
),
102+
ALLOWED_HOSTS=["*"],
103+
)
104+
105+
app = wrap_wsgi_app(get_wsgi_application())
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Copyright 2022 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import json
16+
import re
17+
import subprocess
18+
import uuid
19+
20+
import pytest
21+
import requests
22+
23+
24+
@pytest.fixture
25+
def version():
26+
"""Launch a new version of the app for testing, and yield the
27+
project and version number so tests can invoke it, then delete it.
28+
"""
29+
30+
output = subprocess.run(
31+
f"gcloud app deploy --no-promote --quiet --format=json --version={uuid.uuid4().hex}",
32+
capture_output=True,
33+
shell=True,
34+
)
35+
36+
try:
37+
result = json.loads(output.stdout)
38+
version_id = result["versions"][0]["id"]
39+
project_id = result["versions"][0]["project"]
40+
except Exception as e:
41+
print(f"New version deployment output not usable: {e}")
42+
print(f"Command stderr is '{output.stderr}'")
43+
raise ValueError
44+
45+
yield project_id, version_id
46+
47+
output = subprocess.run(
48+
f"gcloud app versions delete {version_id}",
49+
capture_output=True,
50+
shell=True,
51+
)
52+
53+
54+
def test_upload_and_view(version):
55+
project_id, version_id = version
56+
version_hostname = f"{version_id}-dot-{project_id}.appspot.com"
57+
58+
# Check that version is serving form in home page
59+
response = requests.get(f"https://{version_hostname}/")
60+
assert response.status_code == 200
61+
assert '<form action="' in response.text
62+
63+
matches = re.search(r'action="(.*?)"', response.text)
64+
assert matches is not None
65+
upload_url = matches.group(1)
66+
67+
with open("./main.py", "rb") as f:
68+
response = requests.post(upload_url, files={"file": f})
69+
70+
assert response.status_code == 200
71+
assert b"from google.appengine.api" in response.content
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Copyright 2022 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# Default TEST_CONFIG_OVERRIDE for python repos.
16+
17+
# You can copy this file into your directory, then it will be imported from
18+
# the noxfile.py.
19+
20+
# The source of truth:
21+
# https://github.com/GoogleCloudPlatform/python-docs-samples/blob/main/noxfile_config.py
22+
23+
TEST_CONFIG_OVERRIDE = {
24+
# You can opt out from the test for specific Python versions.
25+
"ignored_versions": ["2.7", "3.6"],
26+
# Old samples are opted out of enforcing Python type hints
27+
# All new samples should feature them
28+
"enforce_type_hints": False,
29+
# An envvar key for determining the project id to use. Change it
30+
# to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a
31+
# build specific Cloud project. You can also use your own string
32+
# to use your own Cloud project.
33+
# "gcloud_project_env": "GOOGLE_CLOUD_PROJECT",
34+
"gcloud_project_env": "BUILD_SPECIFIC_GCLOUD_PROJECT",
35+
# If you need to use a specific version of pip,
36+
# change pip_version_override to the string representation
37+
# of the version number, for example, "20.2.4"
38+
"pip_version_override": None,
39+
# A dictionary you want to inject into your test. Don't put any
40+
# secrets here. These values will override predefined values.
41+
"envs": {},
42+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pytest==7.1.2
2+
requests==2.28.1
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Django==3.2.8
2+
django-environ==0.7.0
3+
google-cloud-logging==2.6.0
4+
appengine-python-standard>=0.2.3
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# This file specifies files that are *not* uploaded to Google Cloud Platform
2+
# using gcloud. It follows the same syntax as .gitignore, with the addition of
3+
# "#!include" directives (which insert the entries of the given .gitignore-style
4+
# file at that point).
5+
#
6+
# For more information, run:
7+
# $ gcloud topic gcloudignore
8+
#
9+
.gcloudignore
10+
# If you would like to upload your .git directory, .gitignore file or files
11+
# from your .gitignore file, remove the corresponding line
12+
# below:
13+
.git
14+
.gitignore
15+
16+
# Python pycache:
17+
__pycache__/
18+
# Ignored by the build system
19+
/setup.cfg

0 commit comments

Comments
 (0)