Skip to content
This repository was archived by the owner on Oct 1, 2025. It is now read-only.

Commit 2510257

Browse files
committed
cleanup READMEs and add push task queue repos
1 parent 1ed94f5 commit 2510257

File tree

22 files changed

+1116
-14
lines changed

22 files changed

+1116
-14
lines changed

README.md

100755100644
Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Use of GCP products & APIs is not free. While you may not have needed to enable
1919

2020
## Support for Python 2 & 3
2121

22-
It's important to note that for App Engine (Standard), Python 2 is only supported as a 1st generation ("Gen1") runtime whereas Python 3 is only supported by the 2nd generation ("Gen2") runtime. This means that porting application from Python 2 to 3 also means migrating from Gen1 to Gen2 where things are different. (Python 2 belongs in the same class as Java 6-8, PHP 5, and Go 1.8-1.11 first-gen apps.) See the exact differences on the [App Engine runtime documentation page](https://cloud.google.com/appengine/docs/standard/runtimes). The most notable changes for developers are that bundled App Engine built-in services are absent from Gen2. The Gen1 bundled services have "grown-up" to become standalone products or have been deprecated. Gen2 also expects web apps to perform their own application (not network) routing.
22+
It's important to note that for App Engine (Standard), Python 2 is only supported as a 1st generation ("Gen1") runtime whereas Python 3 is only supported by the 2nd generation ("Gen2") runtime. This means that porting application from Python 2 to 3 also means migrating from Gen1 to Gen2 where things are different. (Python 2 belongs in the same class as Java 6-8, PHP 5, and Go 1.8-1.11 first-gen apps.) See the exact differences on the [App Engine runtime documentation page](https://cloud.google.com/appengine/docs/standard/runtimes). One key change: bundled App Engine built-in services are absent from Gen2 (they have either matured to become standalone products or have been deprecated). The other key change: you must use web frameworks that do their own routing.
2323

2424
> **NOTE:** App Engine ([Flexible](https://cloud.google.com/appengine/docs/flexible/python/runtime?hl=en#interpreter)) is a Gen2 service but is not within the scope of these tutorials. Developers who are curious can compare App Engine [Standard vs. Flexible](https://cloud.google.com/appengine/docs/the-appengine-environments).
2525
@@ -44,7 +44,7 @@ Each major migration step has its own codelab & corresponding overview video. Th
4444
1. **Migrate from Cloud NDB to [Cloud Datastore](http://cloud.google.com/datastore)** ([2.x](/step3-flask-datastore-py2) or [3.x](/step3-flask-datastore-py3))
4545
- Only recommended if using Cloud Datastore elsewhere (GAE *and* non-App Engine) apps
4646
- Helps w/code consistency & reusability, reduces maintenance costs
47-
- Can migrate to [Cloud Firestore](http://cloud.google.com/firestore) after this step ([Step 3a](/migrate-python2-appengine/tree/master/step3a-flask-firestore-py2); 3.x-only)
47+
- Can migrate to [Cloud Firestore](http://cloud.google.com/firestore) after this step ([Step 3a](/step3a-flask-firestore-py2); 3.x-only)
4848
- **Very** optional: infrequent/uncommon & "expensive" migration
4949
- Requires new project & Datastore has better write performance (currently)
5050
- If you **must have** Firestore's Firebase features
@@ -55,7 +55,7 @@ Each major migration step has its own codelab & corresponding overview video. Th
5555

5656
Think of it as a train ride where the first pair of stops are required, then the passengers can "get off" at any upcoming station or continue their onward journey.
5757

58-
> **ATTN mobile developers:**
58+
### Considerations for mobile developers
5959
If your original app users does *not* have a user interface, i.e., mobile backends, etc., but still uses `webapp2` for routing, some migration must still be completed. Your options:
6060
- Migrate to Flask (or another) web framework but keep app on App Engine
6161
- Use Cloud Endpoints for your mobile endpoints
@@ -64,7 +64,7 @@ If your original app users does *not* have a user interface, i.e., mobile backen
6464
- [Firebase mobile & web app platform](https://firebase.google.com) (and [Cloud Functions for Firebase](https://firebase.google.com/products/functions) [customized for Firebase])
6565

6666
> **NOTE:**
67-
- Long-time users wishing to bring back their dead/deprecated apps on the original Python 2.5 runtime ([deprecated in 2013](http://googleappengine.blogspot.com/2013/03/python-25-thanks-for-good-times.html) and [shutdown in 2017](https://cloud.google.com/appengine/docs/standard/python/python25) must [migrate from `db` to `ndb`](http://cloud.google.com/appengine/docs/standard/python/ndb/db_to_ndb) (and 2.5 to 2.7) before attempting any of these migrations.
67+
Long-time users wishing to bring back apps on the inaugural Python 2.5 runtime, [deprecated in 2013](http://googleappengine.blogspot.com/2013/03/python-25-thanks-for-good-times.html) and [shutdown in 2017](https://cloud.google.com/appengine/docs/standard/python/python25), must [migrate from `db` to `ndb`](http://cloud.google.com/appengine/docs/standard/python/ndb/db_to_ndb) (and 2.5 to 2.7) before attempting these migrations.
6868

6969
## Summary
7070

@@ -76,16 +76,16 @@ Python 2 | Next | Python 3 | Description
7676
[`step1-flask-gaendb-py2`](/step1-flask-gaendb-py2) | ↓ | _N/A_ | Migrate to Flask
7777
[`step2-flask-cloudndb-py2`](/step2-flask-cloudndb-py2) | ↓ or → or ⤓ª | [`step2-flask-cloudndb-py3`](/step2-flask-cloudndb-py3) | Migrate to Cloud NDB
7878
[`step3-flask-datastore-py2`](/step3-flask-datastore-py2) | ↓ or → or ⤓+º | [`step3-flask-datastore-py3`](/step3-flask-datastore-py3) | Migrate to Cloud Datastore
79-
[ª`step4-cloudndb-cloudrun-py2`](/step4-cloudndb-cloudrun-py2) | → | [`step4-cloudds-cloudrun-py3`](/step4-cloudds-cloudrun-py3) | Migrate to Cloud Run (with Docker)
8079
_N/A_ | _N/A_ | º[`step3a-flask-firestore-py3`](/step3a-flask-firestore-py3) | Migrate to Cloud Firestore (uncommon; see above)
80+
[ª`step4-cloudndb-cloudrun-py2`](/step4-cloudndb-cloudrun-py2) | → | [`step4-cloudds-cloudrun-py3`](/step4-cloudds-cloudrun-py3) | Migrate to Cloud Run (with Docker)
8181
_N/A_ | _N/A_ | +[`step4a-cloudrun-bldpks-py3`](/step4a-cloudrun-bldpks-py3) | Migrate to Cloud Run (with Cloud Buildpacks)
8282

8383
### Canonical code samples
8484

85-
- These sample app code samples were made specifically for the corresponding codelabs & videos.
86-
- The canonical migration code samples are those in the [official migration documentation](https://cloud.google.com/appengine/docs/standard/python/migrate-to-python3)
87-
- [Canonical migration code samples repo](https://github.com/GoogleCloudPlatform/python-docs-samples/tree/master/appengine/standard/migration)
88-
- Example: [GAE NDB to Cloud NDB migration sample](https://github.com/GoogleCloudPlatform/python-docs-samples/tree/master/appengine/standard/migration/ndb/overview)
85+
- This repo, along with corresponding codelabs & videos are complementary to the official docs & code samples.
86+
- The [official Python 2 to 3 migration documentation](https://cloud.google.com/appengine/docs/standard/python/migrate-to-python3)
87+
- [Canonical migration code samples repo](https://github.com/GoogleCloudPlatform/python-docs-samples/tree/master/appengine/standard/migration)
88+
- Example: [GAE NDB to Cloud NDB](https://github.com/GoogleCloudPlatform/python-docs-samples/tree/master/appengine/standard/migration/ndb/overview)
8989

9090
## Next
9191

step2-flask-cloudndb-py3/README.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ In addition to *actually* porting your app from Python 2.x to 3.x, you would als
1919

2020
### Configuration
2121

22+
#### Simplify `app.yaml`
23+
2224
The only real change for this sample app is to significantly shorten `app.yaml` down to just these lines for the runtime as well as routing:
2325

2426
```yml
@@ -29,10 +31,7 @@ handlers:
2931
script: auto
3032
```
3133
32-
An additional improvement you can make is to get rid of the `handlers:` section altogether (especially since `script: auto` is the only accepted directive regardless of URL path) and replace it with an `entrypoint:` directive. In Gen1, handlers were necessary to help route requests to your app, but in Gen2, routing is the responsibility of the web framework, not as an App Engine configuration.
33-
34-
35-
TODO
34+
An additional improvement you can make is to get rid of the `handlers:` section altogether (especially since `script: auto` is the only accepted directive regardless of URL path) and replace it with an `entrypoint:` directive. In Gen1, handlers were necessary to help route requests to your app, but Gen2 requires routing be done by the web framework, not as an App Engine configuration.
3635

3736
If you do that, you're `app.yaml` may look like the following (assuming there is a "main" function in your `main.py` which we not have in ours until Step 4):
3837

@@ -43,7 +42,12 @@ entrypoint: python main.py
4342

4443
Check out [this page in the documentation](https://cloud.google.com/appengine/docs/standard/python3/runtime#application_startup) to find out more about the `entrypoint:` directive for `app.yaml` files.
4544

46-
The `requirements.txt` and `templates/index.html` files remain unchanged while the `appengine_config.py` file and `lib` folder are deleted.
45+
46+
#### Delete `appengine_config.py` and `lib`
47+
48+
One of the welcome changes on the second generation of App Engine runtimes is that Bundling/vendoring of third-party packages is no longer required from users. No built-in libraries (per the changes to `app.yaml` above), no `appengine_config.py` file nor `lib` folder.
49+
50+
Delete the `appengine_config.py` file and `lib` folder now. The `requirements.txt` and `templates/index.html` files remain unchanged.
4751

4852
---
4953

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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+
11+
# Ignore source code control maintenance files
12+
.git
13+
.gitignore
14+
.hgignore
15+
.hg/
16+
17+
# Python files
18+
*.pyc
19+
*.pyo
20+
__pycache__/
21+
/setup.cfg
22+
23+
# no need to upload README
24+
README.md
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# Step 5a - Add Push Tasks to App Engine `webapp2` & `ndb` sample app
2+
3+
## Introduction
4+
5+
The goal of the Step 5 series of codelabs and repos like this is to help App Engine developers migrate from [Python 2 App Engine (Push) Task Queues](https://cloud.google.com/appengine/docs/standard/python/taskqueue/push) to [Google Cloud Tasks](https://cloud.google.com/tasks). They are meant to be *complementary* to the official [migrating push queues to Cloud Tasks documentation](https://cloud.google.com/appengine/docs/standard/python/taskqueue/push/migrating-push-queues) and [corresponding code samples](https://github.com/GoogleCloudPlatform/python-docs-samples/tree/master/appengine/standard/migration/taskqueue) and offer some additional benefits:
6+
- Video content for those who prefer visual learning in addition to reading
7+
- Codelab tutorials give hands-on experience and build "migration muscle-memory"
8+
- More code samples gives developers a deeper understanding of migration steps
9+
10+
In this codelab/repo, participants start with the code in the (completed) [Step 1 repo](https://github.com/googlecodelabs/migrate-python-appengine-datastore/tree/master/step1-flask-gaendb-py2) where developers migrated to the Flask web framework and add support for Push Task Queues using the App Engine `taskqueue` API library. As you will recall, the sample app registers each visit (`GET` request to `/`) by creating a new `Visit` Entity for it then fetches and displays the 10 most recent `Visit`s in the web UI.
11+
12+
This codelab step adds a push task to delete all `Visit`s older than the oldest entry. If you haven't completed the [Step 1 codelab](https://codelabs.developers.google.com/codelabs/cloud-gae-python-migrate-1-flask), we recommend you do so to familiarize yourself with the Step 1 codebase.
13+
14+
---
15+
16+
## Augment sample with new feature
17+
18+
Rather than a migration, this step adds use of push Task Queues to the existing Step 1 app. The only modifications required are for the `main.py` application file and `templates/index.html` web template. The steps:
19+
20+
1. Add new Python `import`s
21+
- Add use of Python standard library date and time utilities
22+
- (optional but helpful) Add logging and function ["docstrings"](http://python.org/dev/peps/pep-0257/#id15)
23+
1. Save timestamp of last (displayed) `Visit`
24+
1. Add "delete old(est) entries" task
25+
1. Display deletion message in web UI template
26+
27+
### Add new Python `import`s
28+
29+
It's useful to add logging to applications to give the developer (and the user) more information (as long as it's useful). For Python 2 App Engine, this is done by using the Python standard library `logging` module. For date & time functionality, add use of the `datetime.datetime` class as well as the `time` module.
30+
31+
- BEFORE:
32+
33+
```python
34+
from flask import Flask, render_template, request
35+
from google.appengine.ext import ndb
36+
```
37+
38+
The Python best practices of alphabetized group listing order:
39+
1. Standard library modules first
40+
1. Third-party globally-installed packages
41+
1. Locally-installed packages
42+
1. Application imports
43+
44+
Following that recommendation, your imports should look like this when done:
45+
46+
- AFTER:
47+
48+
```python
49+
import logging
50+
from datetime import datetime
51+
import time
52+
from flask import Flask, render_template, request
53+
from google.appengine.api import taskqueue
54+
from google.appengine.ext import ndb
55+
```
56+
57+
### Save timestamp of last (displayed) `Visit`
58+
59+
The `fetch_visits()` function queries for the most recent visits. Add code to save the timestamp of the last `Visit`.
60+
61+
- BEFORE:
62+
63+
```python
64+
def fetch_visits(limit):
65+
return (v.to_dict() for v in Visit.query().order(
66+
-Visit.timestamp).fetch(limit))
67+
```
68+
69+
Instead of immediately returning all `Visit`s, we need to save the results, grab the last `Visit` and save its timestamp, both as a `str`ing (to display) and `float` (to send to the task).
70+
71+
- AFTER:
72+
73+
```python
74+
def fetch_visits(limit):
75+
'get most recent visits & add task to delete older visits'
76+
data = Visit.query().order(-Visit.timestamp).fetch(limit)
77+
oldest = time.mktime(data[-1].timestamp.timetuple())
78+
oldest_str = time.ctime(oldest)
79+
logging.info('Delete entities older than %s' % oldest_str)
80+
taskqueue.add(url='/trim', params={'oldest': oldest})
81+
return (v.to_dict() for v in data), oldest_str
82+
```
83+
84+
The `data` variable holds the `Visit`s previously returned immediately, and `oldest` is the timestamp of the oldest displayed `Visit` in seconds (as a `float`) since the epoch, retrieved by (extracting `datetime` object, morphed to Python [time 9-tuple normalized form](https://docs.python.org/library/time), then converted to `float`). A string version is also created for display purposes. A new push task is added, calling the handler (`/trim`) with `oldest` as its only parameter.
85+
86+
The same payload as the Step 1 `fetch_visits()` is returned to the caller in addition to `oldest` as a string. Following good practices, a function docstring was added (first unassigned string) along with an application log at the `INFO` level via `logging.info()`.
87+
88+
### Add "delete old(est) entries" task
89+
90+
While deletion of old `Visit`s could've easily been accomplished in `fetch_visits()`, this was a great excuse to make it a task which is handled asynchronously after `fetch_user()` returns, and the data is presented to the user. This improves the user experience because there is no delay in waiting for the deletion of the older Datastore entities to complete.
91+
92+
```python
93+
@app.route('/trim', methods=['POST'])
94+
def trim():
95+
'(push) task queue handler to delete oldest visits'
96+
oldest = request.form.get('oldest', type=float)
97+
keys = Visit.query(
98+
Visit.timestamp < datetime.fromtimestamp(oldest)
99+
).fetch(keys_only=True)
100+
nkeys = len(keys)
101+
if nkeys:
102+
logging.info('Deleting %d entities: %s' % (
103+
nkeys, ', '.join(str(k.id()) for k in keys)))
104+
ndb.delete_multi(keys)
105+
else:
106+
logging.info('No entities older than: %s' % time.ctime(oldest))
107+
return '' # need to return SOME string w/200
108+
```
109+
110+
Push tasks are `POST`ed to the handler, so that must be specified (default: `GET`). Once the timestamp of the `oldest` visit is decoded, a Datastore query to find all entities strictly older than its timestamp is created. None of the actual data is needed, so a faster "keys-only" query is used. The number of entities to delete is logged, and the deletion command (`ndb.delete_multi()`) given. Logging also occurs if there are no entities to delete. A return value is necessary to go along with the HTTP 200 return code, so use an empty string to be efficient.
111+
112+
113+
### Display deletion message in web UI template
114+
115+
It's also a good practice to have "docstrings" to document functionality, so add those as well. Add the following snippet after the unnumbered list of `Visit`s but before the closing `</body>` tag:
116+
117+
```html+jinja
118+
{% if oldest %}
119+
<b>Deleting visits older than:</b> {{ oldest }}</p>
120+
{% endif %}
121+
```
122+
123+
### Configuration
124+
125+
There are no changes to any of the configuration (`app.yaml`, `appengine_config.py`, `requirements.txt`) files.
126+
127+
---
128+
129+
## Next
130+
131+
Try the app in the local development server (`dev_appserver.py app.yaml`), debug (if nec.), and deploy to App Engine and confirm everything still works. Once you're satisfied, move onto the next step:
132+
133+
- [**Step 5b:**](/step5b-cloud-ndb-tasks-py2) Migrate your app away from App Engine built-in libraries like `ndb` &amp; `taskqueue` to Cloud NDB &amp; Cloud Tasks

step5a-gae-ndb-tasks-py2/app.yaml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Copyright 2020 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: python27
16+
threadsafe: yes
17+
api_version: 1
18+
19+
handlers:
20+
- url: /.*
21+
script: main.app
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Copyright 2020 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 google.appengine.ext import vendor
16+
17+
# Set PATH to your libraries folder.
18+
PATH = 'lib'
19+
# Add libraries installed in the PATH folder.
20+
vendor.add(PATH)

0 commit comments

Comments
 (0)