Python App Engine Codelab

Maintained by GoogleDeveloper Relations

Aug 2013

Objective/Description:This codelab will demonstrate how to build applications using the Python version of Google App Engine. It is broken down into three sections. After completing this codelab, you should be able to create (and deploy) applications on Google App Engine using Python.

Part 1 Focuses on the basics which is to build a simple guestbook application. Incidentally, this application is based on the existing "Getting Started" online tutorial in the official docs, but it also extends upon it a bit as well as offers more detailed explanations.

Part 2 Focuses on providing a set of examples of how to use the various App Engine services such as email, XMPP.

Part 3 Shows how to use URLFetch, Cron, Task Queues and the Channel API to push browser updates from the server. This is a standalone lab and can be run independently from the first two labs.

Requirements:
You have (at least) two options to do this codelab:

Option 1 Download SDK & Develop Locally (preferred; deploy option)

A laptop/notebook computer... you can share with others if you wish

Python 2.7.x (also see the separate mostly-equivalent Java codelab)

Python should already be available on Mac & Linux (or other POSIX) systems...

This codelab is based on the online tutorial found at http://developers.google.com/appengine/docs/python/gettingstarted. That "Hello World" tutorial for Google App Engine involves the creation and deployment of a web-based "guest book" application. If you follow the tutorial, you'll see "Run/Modify" buttons on the code samples -- clicking on those buttons will drop you into the Cloud Playground with that snippet of code for you to experiment with. If desired, you can do that tutorial first before coming here. This codelab takes that existing tutorial and expands on it by offering more of an in-depth explanation and step-by-step process.

Alternatives

If jumping directly into code like we do here is too overwhelming, and you wish to get more background and move more slowly, go to the Google Developers Academy where you can take the intro to cloud computing & App Engine class first then do that Python 101 class afterwards. Both the tutorial and Academy classes will get you about halfway through part 1 of this codelab.

Part I: Getting Started

Option 1: Setup & the Launchers

This “preliminary” lab is all about getting things setup and working. We will be using the development server that comes with the SDK so Internet connectivity is not necessary (unless you need to download the SDKs and related software such as Eclipse). By the end of this short “lab,” you should be able to create a new project, run the development server on the auto-generated application “stub” and be able to visit the app athttp://localhost:8080(or whatever port you started up your app on).

The specific tasks to accomplish here:

Get/install Python if necessary

Get/install the App Engine SDK

Decide whether to use the Launcher UI (or not)

Decide whether you will be on the cmd-line or use an IDE

Create the application handler file (main.py)

Create the application configuration file (app.yaml)

Learn how to start the SDK development server

If you bring up the launcher and create a new application, it will create your files automatically and give you 1-click ability to start the server. They will look like the following on Win32 and Mac OS X systems:

Figure 1a. Google App Engine Launcher on PCs

Figure 1b. Google App Engine Launcher on Mac OS X

Regarding application names, you can choose whatever you want for development, but be aware that you will need to rename it to deploy live. Pick something appropriate and unique. We suggest call it “helloworld” – keep it all lowercase; users have had problems when using CamelCasing their app names.

Option 2: Cloning the Hello World Project

If you're using Cloud Playground, go to the site then copy the Hello World project (circled in the image [Figure 2a] below).

Once you've cloned it, it shows up in your set of projects. Select that you wish to work on it by clicking on the blue arrow button to the left (Figure 2b).

You'll then bring up the project and the app.yaml config file, as you can see here in Figure 2c:

You can also select the main application file main.py too (FIgure 2d):

You'll be learning about these 2 files in the next section.

By the way, we mentioned there are more than 2 options to proceeding with the codelab. If you're not on a PC or Mac and doing Option 1 and not using Option 2's Cloud Playground, you may be using a Linux or some other system purely from the command-line (no UIs at all). In this case, you'll need to enter the app code by hand. It’s not that bad, as you'll see in the upcoming section.

Exploring the Code

Let's explore the main.py code first. It should look something like the below -- BTW, code generated by the Launcher will also feature a bunch of comments at the top with Google copyright notice -- they don't play a part in the codelab so we'll just leave them out. The code you get from the Launcher or the Cloud Playground should look something like:

Now that you have an application handler file, you’re ready to go... that is, if using the Launcher or Cloud Playground, you'll get this in the body of the app.yamlconfiguration file (or enter by hand if using neither):

Option 1: Once you have both of these files, then you’re ready to run the server. It’s as easy as one click in the launcher. If you are on the command-line, you need to run the server manually:

$ dev_appserver.py helloworld

Now you should be able to hithttp://localhost:8080 (the port may be diffierent if using the Launcher, in which case it's easier to just click the Run button in the UI) and see the expected output in your browser:

Figure 3a. “Hello World!” in plain text

Option 2: Those using Cloud Playground do less work as there's already a server running. Just click the run button in the web UI to see the exact same output (in the output window at the bottom), as shown here in Figure 3b:

Figure 3b. “Hello World!” in plain text from the Cloud Playground UI

Please get this working as you need to complete each section of the lab in order to move to the next.

Before moving onto the next part of the lab, make a few final tweaks: the first is to add a comma after the "('/', MainHandler)" 2-tuple to avoid any issues when adding new handlers.

Also, you want to test out the dynamic nature of the development server: enclose "Hello World!" in <h1> tags so as to output HTML instead of plain text. Once you save this change, you shouldn't have to restart the server before hitting your browser and seeing the change immediately:

Figure 4a. "Hello World!" in HTML (H1 header tag)

Optionally, you can also provide the proper MIME header as part of your response... in our case, we could add:self.response.headers['Content-Type']='text/html'as the first line of the handler’sget()method. Those of you doing Option 2 have probably figured out that you can change the code directly in the Cloud Playground, hit the run button and see the changes reflected immediately, like what you see below in Figure 4b.

For the remainder of the codelab, we'll just show the necessary bits for Option 1. Option 2 users can mimic the changes in the code for the same effect as you can pretty much complete all of Part 1 using the Cloud Playground.

Adding web forms

Use triple quotes to add input HTML form with text field

Form action is /sign which does not exist yet... 404 error

In this part of the codelab, we’re going to give your users the ability to input data. Rather than simply displaying “Hello World,” let’s create a simple form with one text field and allow the user to submit it to your app. Part of the form is defining the “action” for it, which is the URL that the browser will POST the form variables to. In this case, it’s/sign. However, since we haven’t defined a handler for it, this request – when a user clicks on the “Sign Guestbook” button – will generate a 404 error, which you will either see in your browser, or it will be blank... it depends complete on your browser.

Leave the “Hello World” line as-is and add the form via another call toself.response.out.write(). Python’s triple quotes (3 single or 3 double quotes) allows users to embed special characters like NEWLINEs directly into a string verbatim. What does this mean for users? No more long string wrapping, awkward backslashes, or ugly string concatentation. You can cut-n-paste blocks of HTML directly into the source. The indentation of the string data does not matter, however, we indent it in a manner consistent with the Python code around it if nothing but for readbility.

To make this happen, add another call right below the "Hello World" output line:

Again, there is no handler for guestbook signing yet, so you will get a 404 error or a blank screen if you click on the button!

Similar to the previous example, the changes here are so trivial that we do not need to show you the entire main.py file.

Accept form input & display to user

Create handler for/sign:GuestBook.post()

Show output: user input

Obviously, the next step is to be able to process the user input. We will create a new handler to handle the/signURL that we created in the previous section of the codelab. We’ll call our new handler classGuestBook, although you’re free to use any name you like. Forms use the HTTP POST method, so that’s why we’re creating apost()method (insetad ofget()).

Outside of a minor<br>tag added to the original form, you’ll be doing 2 things in this lab:

Adding theGuestBookclass – watch the spelling... you don’t need to use CamelCasing as we’ve done here but it should be consistent – and itspost()method which simply outputs the user input.

Adding another 2-tuple when creating the application to define the handler (GuestBook) for/signURLs

As you can see from the image below, the extra “<br>” caused the button to go below the form field.

Figure 6. Filling out a web form

When you submit the form, the “/sign” handler will display the output back to you:

Figure 7. Filling out a web form

One word of caution is that the app (at the moment) accepts all text entered by the user, including characters that make up HTML tags, i.e., <, >, etc. If you want to escape these characters to avoid “injection” attacks, import theescape()function from the Python standard librarycgimodule and wrap the content in it, i.e.,fromcgiimportescapefollowed elsewhere in your code with:self.response.out.write('<h2>Youwrote:</h2>%s'%escape(self.request.get('content')).

Datastore: Saving app data to the cloud

Form data changed totextarea

Create and use new data model class

GET/-> Show output -> fetch & display stored entries

POST/sign–> Store user input in datastore -> redirect to/

UsesQueryinterface to retrieve all objects in datastore

Commented out line uses equivalentGqlQueryinterface

Web applications wouldn’t nearly be as exciting if they didn’t store any data right? The App Engine datastore is arguably the most complex component you will need to learn to become proficient at App Engine, but it is also the single thing that’s the most difficult to implement if you were doing it yourself: creating a reliable, distributed, scalable, persistent data storage mechanism. This isnota relational database with rows and tables... you cannot and should not think of it in this way. It is more like an “object database” with “objects” or “entities.”

In this lab, we will store user input as the first major step at creating our online guestbook.

The home page behavior will also change... instead of just presenting a form for user input, we will show the previous guestbook entries first as well as tweak the HTML a bit. So here are the changes that need to be made:

Those of you who have a sharp eye will notice that one of the lines in get() is commented out. If you’re coming from a relational world, you may feel more at home with App Engine’s GqlQuery interface, letting you use a subset of SQL to issue the same request. You can use that line instead of the query used on line 18 but we don’t recommend that as you need to start getting used to object queries anyway, and now is as good a time as any.

As mentioned above, the home page behavior is now different, and you should see those changes reflected below:

Figure 8. New form and saved data

Figure 9. Form submission redirects to (same) home page

User authentication

Add author field to schema

Authentication with Google Accounts

Show user name if logged in or sign-in link if not

Using the Users service (users API)

get_current_user()returns None if not logged in

create_login_url()returns valid login page link

nickname()method returns logged-in user’s name

At some point, application developers realize it’s a good thing to have unique users rather than treating everyone the same. Google provides its own user authentication services as well as supports alternatives such as OAuth and OpenID. The changes for this section of the lab are less intensive than the previous section.

Our example will use Google’s authentication service, which involves importinggoogle.appengine.api.users(seeline 4above). Theget_current_user()function is used to get the login of the current user. We need this in several places, the first of which is the home page – we want to greet the currently-logged in user (lines 16-23). This call is also required when a user submits an entry (lines 31-33) because this is the only time you can save this fact, and it’s pretty useful information to differentiate who made which posts.

Your app should function before as expected except for a couple of things... the first thing users will now notice is a sign-in link at the top:

Figure 10. New login link at the top

When users click to login, they should see a login prompt similar to this:

Figure 11. User login prompt

After logging in, they’re redirected back to the home page, which now shows user info:

Figure 12. App knows user is logged in

Force user logins

Require users to login before accessing site

Show sign-in and sign-out links as appropriate

Customizing queries

This section of the codelab features a couple of minor yet interesting updates to the application. We’re requiring users be logged in before being able to access the home page. Also, let’s change the query a bit... because a guestbook is a time-sensitive application, let’s sort the entries to newest-first (reverse chronological) order.

The changes you’re making here include:

Last time we added a login link; it’s only fair to add one for logout!

Redirect to the login link if not logged in

Sort by reverse order on the date and only show the ten newest posts

The “trickiest” code snippet is the customization of the query. We provide (onlines 18-21), both the change using the object query as well as the GqlQuery so you can see the difference. Here is the resulting main.py:

As far as behavior changes go, users will now see a logout link at the top:

Figure 13. New logout link at the top

There won’t be a login link any more because users who aren’t are automatically sent to the login screen seen earlier in the previous codelab section. You can confirm this by just clicking on the “sign out” link.

Web templates

Add templating to simplify display

Move all HTML out of code into template (index.html)

Update all variable access for template

Create template context dictionary

Render template w/filename & context

() not necessary for calling functions/methods

Show posts with their authors

It would have been much uglier to put this logic in code

As your application grows both in terms of size, scope, and audience, the need for a visual designer increases. You wish to have the UI be designed by... designers yet want the engineers to stay focused on getting the backend working. This is where web templates come in. In this lab section, we move all of the HTML out into a web template.

App Engine supports a variety of templating systems, but those are separate downloads, and you would need to bundle those with your app when you deploy live. However, the one from Django comes with App Engine and is available in both the SDK as well as in production.

If you’re not familiar with templating systems, they are usually HTML combined with some sense of logic such as if conditional statements and looping mechanisms like a traditional programming for loops. Django’s is no exception. In order to pass the data from the application to the templating system, you need to create what is called a context, meaning the variable namespace for the template itself. You'll see in the new code below and see how all of the HTML (via self.response.out.write() calls) has been removed along with the relevant loops and conditionals. To summarize, for this section of the codelab, the exact steps to take are:

Add a couple of new import statements

Remove the HTML output

Create a template context

Locate the template

Render the template, passing in its context

Below is the (new) template file, which you can call index.html, that contains most of what was removed... we will add anything else back later:

Although the syntax may be foreign to you newbies, you should get the gist of how the template works as well as the syntax of the Django template flow control directives (called Django tags) which is somewhat Python-flavored. You should also observe that callables in Django’s templating system do not need to be called (absence of calling parentheses: ())... they use the pipe (|) symbol and are meant to be similar to Unix&reg; pipes. Such callables are known as Django filters and called automatically. You can learn more at the Django Built-in template tags and filters page as well as the general Django template language page.

The remaining handler code after performing the steps outlined above looks like this:

In the previous section, we started to logically segregate the files which are the responsibility of the engineer as well as the visual designer. In this section, we take it one step further: static file support.

The user experience/interaction designer will want to do more than just creating web templates. Templates are just one example of static files which are used to render web pages. Other static files include Cascading Style Sheets (CSS files) and images, both of which can be used to give all the pages of a website a common look-and-feel.

To keep things organized, we create a top-level static directory containing subdirectories for CSS, HTML, and image files. For our example, we’ll only do the first two. The template we created in the previous section is moved to the static/html subdirectory while a brand new CSS file debuts instatic/css. Here is what you need to do in this section of the codelab:

For the template, you can see below that we’ve added back the login and logout links, hence the reason why we required the additional context variables above. There is also a reference to our new CSS file,main.css, as well. The other big change is that instead of just enumerating and displaying the guestbook entries, we prepend each post with a timestamp of when the post was made as well as the author (if known, or “anonymous” if not). Our guestbook is starting to look a little more “professional” now!

These two lines need to be placed on top of your original ones because the latter handles all requests (via its universal regular expression ".*"). You want the /static/css match to come first so it won't be "caught" by the universal one.

It’s time for a double feature: in this codelab section, we’re going to add caching using the App Engine Memcache API as well as introduce template inheritance.

Fetching lots of data from the datastore can be time-consuming. Applications generally see a marked increase in performance by caching often used and accessed data. Memcache is merely a hash table in the cloud, and comes both with a simple API as well as more intermediate features that you expect. In our application, we’re going to cache the set of guestbook entries. Specifically, we will cache them for ten seconds – this is sufficiently long enough so that you can observe browser refreshes hitting the memcache but isn’t long enough to let the data in the cache go stale.

Another goal of this section further addresses the “common look-and-feel” theme we alluded to earlier. It is desireable that web pages have the same general overall look along with the same header and footer links. It makes no sense to put the same HTML in multiple HTML files when they can all share a “boilerplate” original. In our simple app, this comprises just the “Hello World” greeting at the top along with login or logout link and the “import” of the CSS file – these would be shared by all templates which extend from the base.

The diffs are straightforward, so you need to do the following:

Import the Memcache API package

Code to check if we have a cache hit

If not in-cache, we need to fetch & cache data

New entries auto-flushes the (now) stale cache

The complete handler which includes the changes from the previous as well as this section:

In the base template above, you will see that there is room reserved for the actual content, e.g., in the Django block tag. Below in the stripped page content file, index.html, we extend the base template and specify the content to drop into the base template when rendered. Otherwise, it’s exactly the same as the template file from the previous codelab section:

In a more realistic situation, you would have a UE/UI/UX graphic designer working on the CSS independently of the developer, allowing various team members to work on tasks simultaneously. The only file they may both access would be the web template.

Once you’ve made the modifications above and have a running app, be sure to make a new entry into your datastore. Then refresh your page several times and visit the memcache viewer on the admin console to confirm you’re hitting the cache as well as fetching and re(caching) after 10s have elapsed. You can also confirm the cache is flushed as-expected when a new post is made because the redirect to the home page should show the new post right away.

Part 2: Using other App Engine Services: email, XMPP

Now that we have a basic guestbook running, there are some optional features we can add to our app with just a few simple changes. App Engine provides a rich set of APIs to give you more flexibility in your applications. We will take a look at:

Sending email (receiving email also supported)

Chat/IM/Jabber (XMPP)

Logging (coming soon)

Other App Engine APIs not covered in this codelab include:

Application statistics (AppStats)

Blobstore

Task queues/cron/deferred

URLfetch

Profiling

Capabilities

remote_api

Other things you can do outside of the App Engine APIs include:

Twitter

PubSubHubBub

Google Wave bot

Sending email

Sending email is quite easy with App Engine. There are necessary restrictions, but it can be as simple as this code snippet:

Be sure to substitute in the correct values for the sender and receive above before integrating into your application. What is in the code snippet above will not compile.

App Engine also supports the receipt of email, but that is another topic for another time. Anyway, it should be fairly straightforward to upgrade your guestbook by adding an email feature. Here are some ideas as to what you can do to your app to enhance it with email:

The administrator when anyone makes a post

All previous posters when anyone makes a post

A receipt to a poster that their post was approved/published/stored

For our guestbook example, we'll do the first item in the list above:

Email the administrator for every new post (supporting anonymous posts)

Another form of communication that people use these days is instant messaging. XMPP (Extensible Messaging and Presence Protocol) is an open XML-based protocol (originally named Jabber) that enables this type of communication between users (and chat robots)!

Similar to email, activity is only logged via the development server. You really need to upload it live to experience this functionality.

Sending IMs can be as simple asxmpp.send_message(jid,msg)where a “jid” is synonymous to a “Jabber ID” or the user you’re trying to communicate with, andmsgis a string representing the message itself. However, things aren’t always as simple. You usually have to first check if a user is accepting your message, and if a user is online before sending an IM. In order to do this, we define Subscriber class for tracking users' subscription/online status, and check is_friend and status properties first.

Receiving of messages requires a separate handler, because when an IM comes in to your app, App Engine POSTS to http://APP-ID.appspot.com/_ah/xmpp/message/chat/, thus you have to handle those inbound requests. Likewise, subscription notifications are sent to http://APP-ID.appspot.com/_ah/xmpp/subscription/SUBSCRIPTION_STATUS/ where SUBSCRIPTION_STATUS is one of subscribe/subscribed/unsubscribe/unsubscribed, and online status notifications are sent to http://APP-ID.appspot.com/_ah/xmpp/presence/ONLINE_STATUS where ONLINE_STATUS is one of available/unavailable/probe(probe is for requesting user's online status, not a notification). Please see more details at User SubscriptionsandUser Presencein our documentation.

In the application below, we’ve built both a sending and receiving of IMs. The sending occurs if a user is recognized as being friend, and online; if the user is not a friend, an invitation to chat is sent instead, and if the user is a friend, but offline, then nothing happens. On the receiving side, we simply capture the inbound message and immediately turn it around and reply to the send that we received their message (and what it was).

Allxmppfunctionssend_invite(), andsend_message()also take afrom_jididentifier. Similar to email, this can be eitherAPP-ID@appspot.comorxxx@APP-ID.appspotchat.com(yes,xxxcan be anything and be sure to reread that domain name!) If not provided to these functions, it defaults toAPP-ID@appspot.com.

It’s also not inconceivable that we could add IM features to the guestbook application like the ones for email we suggested above:

A simple “ping” could be sent to the guestbook owner notifying of a new entry

A user could add an entry to the guestbook by sending an IM to the “guestbook” app/user

Instant messaging is a popular and useful form of communication like email is, but unlike email, there is more of a factor of instant gratification.

One good example of extending the simple XMPP app above as well as our GuestBook application is to add a “chatbot” that takes a few simple commands, one of which returns the top 5 most recent guestbook entries:

The most notable new additions to this version ofmain.py are the newGBChatBotclass and the creation of a separate function to get the greetings we need to return to the user (5 for the chatbot and 10 for the website). Here are a summary of all the changes we had to make from the previous version:

(line 2) Importing thexmppmodule

(lines 12-17, 22, 57) Need single routine to fetch data

(lines 52-75) NewGBChatBotclass

(line 80) New handler for inbound XMPP messages

NOTE: the application at this point willnotaccept instant messages yet. You need to be invited to be able to chat with the bot. To make this happen, just run the first XMPP app (the simple one before we integrated it into our GuestBook app). If you hit that app at ‘/’, you will receive an invite. We will leave an exercise to the reader to add an opt-in feature to the guestbook app, perhaps when users create a guestbook entry. Once the invite is sent, you can go to your XMPP application and accept the chat request from your app/bot; then you will be able to send commands to it.

To access the entire .html file used, just grab the file from the ZIP file above or jump over to the Java codelab momentarily.

Using URLFetch to retrieve an XML feed

This section will show how to extract content from an XML feed. In this example a news feed will be fetched and displayed. A further exercise will fetch the feed and persist it in the datastore for later use.

The handler you should create -- we'll call ours FetchNewsHandler -- will fetch an XML news feed, parse the data, and persist it in the datastore. Although it could be run manually, it is best suited to be run by a cron job as it will continually refresh the datastore with the latest news feeds. These steps explain how to build this servlet.

create a NewsItem data model

create a handler to fetch news from an XML RSS feed

# feed item data model

class NewsItem(db.Model):

. . .

# fetches an XML RSS news feed and displays it

class FetchNewsHandler(webapp2.RequestHandler):

def get(self):

# delete old news items

. . .

# get, parse, and store feed items; we

# suggest xml.dom, xml.etree in stdlib or

# 3rd-party: BeautifulSoup, lxml, html5lib

. . .

# display stored feed items

# (for debugging; remove for prod)

. . .

Building a Servlet to create Channel API Tokens for client access

This handler will respond to a JavaScript client and create a new Channel API Client ID and along with a token which is then returned back to the client so it can set up a communication channel.

somehow persist the client ID by using the datastore so the server can keep track of all connections

create a channel using the API

return the token

class TokenHandler(webapp2.RequestHandler):

def get(self):

. . .

Building a Handler to Broadcast News headlines to clients

This handler, will broadcast the headlines that have been persisted in the datastore to any browser clients. It first queries the current clients id (that have also been persisted in the datastore), and then queries the news items. It then loops through each news item and broadcasts to all clients.

Although this servlet can be run manually, it is best to be launched as an offline process using a task Queue. A final step will show to make this servlet launchable via a task queue.

Break this out into a function if you want to run it as a deferred task.

create a MessageHandler class

query all the NewsItems

broadcast to all clients by sending a msg to all the channels

(by looping through all the client IDs you saved earlier)

class MessageHandler(webapp2.RequestHandler):

def get(self):

. . .

Installing and running the complete Application

Installing and running the complete application can be done in order to see the entire set of services in this section working together. These steps can be done even before doing the individual steps to build this application.

Download the sample application from the link above.

Unzip this file into a clean directory.

Start up dev_appserver or the launcher on it

Running the app.

(Important, upon first run, you must first seed the News feed database.)

In the console, you should that http://localhost:8080 (or whatever port you started the dev is now accessible.

Important before accessing the application at the root directory, you must first seed the datastore with news feeds. This is done by executing the following "cron" job manually

From the main page in your browser, click on "Login to view news client".

(Important) If you are signed in, click on "sign out" link. Do not click on "proceed to news client" link.

Once you've signed out, click on the "Sign in" link and when the login page appears, select the "Sign in as Administrator" check box, then click "Log In" button.

After logging in as an administrator, manually launch the cron job to fetch news feed data by accessing: http://localhost:8080/cron/fetchNewsThis will fetch the current news feed data and persist it into the local datastore for further use. You should also see the data fetched displayed in the browser window.

Optional: Go to the local SDK Console and verify that the data has been persisted.

http://localhost:8080/_ah/admin/datastore

Now that the local datastore has been populated with new data, we can now access it from the application. Return to the URL: http://localhost:8080

If you are not signed in, please sign in.

Upon verifying that you are signed in, click on the link "proceed to news client". (newsclient.html)

You should see the page "Breaking News Stories" as a header. You should also see browser Alert popup indicating that you are now 'connected to the server'. - Click ok.

Now you can launch the Task that will start broadcasting out a series of news headlines to this browser. To launch this task, open a new tab and access the url: http://localhost:8080/enqueueMsgsThis will launch a task queue that will start broadcasting messages to any clients that are currently logged in.

Now return to the new client page (newsclient.html) in the other tab.

You should now see news headlines and updates appearing in the browser window every 3 seconds!

The End

We hope that you’ve gotten a useful App Engine hands-on tutorial. There is plenty of documentation to read regarding the existing online tutorial, and we have tried to add more specifics to the exercise of getting the Guestbook application running.