Tutorial¶
This guide is aimed at getting started with itty3
. You should be at least
basically familiar with Python & HTTP in order to get the most out of this
tutorial.
Installation¶
For most users in a modern Python environment they can control, you’ll want to run:
$ pip install itty3
Ideally, this should be run within a virtualenv
/pipenv
/similar to
give you some isolation. Installing Python libraries globally is a recipe
for pain & there are many good options out there that are easy/quick to learn
to help you avoid dependency hell.
That said, itty3
is small/minimal by design. The code (minus docstrings
& comments) is less than 1k lines of code. This allows you to embed itty3
directly into your application by copying in itty3.py
(or even within
your app file directly!).
Regardless, itty3
’s only dependency is Python 3. It’s been tested on
Python 3.7.4+, but may work on older versions.
Finally, you can find the complete source for this tutorial within
the examples/tutorial_todolist directory of itty3
’s source code.
Starting An App¶
Once you have installed itty3
, the next thing you’ll want to do is start
an application file. A good choice for starting out is to create an
empty app.py
file.
You’ll start out by importing itty3
:
import itty3
Next, define a module-wide App object instance:
app = itty3.App()
This will allow you start registering “routes”, a combination of an HTTP method, a URI path & a view (a callable function that takes a request & produces a response).
After that, provide a way for the built-in server code to run your new
app
. At the bottom of the file, add the following:
if __name__ == "__main__":
app.run()
This tells the Python interpreter that, if the file itself is being run by
Python, to execute the code within the if
block. The app.run()
runs the built-in development server & starts listening for requests.
Your complete app.py
file should now look like:
import itty3
app = itty3.App()
if __name__ == "__main__":
app.run()
Now run the server using python app.py
. You should get a message about
the server starting & where it’ll handle requests at:
itty3 <version>: Now serving requests at http://127.0.0.1:8000...
You can now go to the browser & hit http://127.0.0.1:8000/ to access the app!
However, because we haven’t written any application-specific code, you’re going to get a 404 Not Found page, which isn’t super-useful. Let’s fix that…
Adding Your First View¶
A “view” function is any callable that can accept a request
object &
produce a response
object for serving back to the user. We don’t
need to focus on the details of those for now (more later), so let’s just
write a basic index view:
import itty3
app = itty3.App()
# NEW CODE HERE!
@app.get("/")
def index(request):
body = "Hello, world!"
return app.render(request, body)
if __name__ == "__main__":
app.run()
The first line (@app.get("/")
) is a decorator. That automatically
registers your index
view with the app
, telling it that the attached
view can handle an HTTP GET
request at the path /
.
The next line (def index(request):
) declares the callable that is
run when that request is seen. In simple itty3
applications, that’s
usually just a plain Python function, but any callable (instance methods,
entire classes, etc.) will work. The only guideline is that it should accept
a request
parameter (a itty3.HttpRequest
object) as the first
positional argument.
We then build a simple string body, just to see a customized result out on the page.
The final piece is the return app.render(request, body)
line. This builds
and returns a HttpResponse
object, which handles all the details of
serving a proper HTTP response back to the user, including status codes &
headers. app.render(...)
is simply a convenience function for producing
that object.
If you Ctrl+C
the python app.py
& restart it, then hit
http://127.0.0.1:8000/ in your browser, you should now get a simple page
that says Hello, world!
.
Hurray! Our first real web response works!
Note
You’ll note that “render”-ing in the context of itty3
doesn’t
do any templating. itty3
doesn’t care how you produce response
bodies, as long as a string or iterable can be returned.
This opens a world of options, from reading/returning entire files
(good for Single-Page Apps), returning serialized data (like JSON, YAML
or XML), or using your own choice of template language (like Jinja2
,
Mako
or even regular Python format strings).
The downside is that you need to do a bit more work & make a choice around what you want to do. Evaluate your options & choose the one that works for you.
Building A TODO List App¶
Let’s step beyond this & start crafting a real interactive app. We’ll build a very basic TODO list application.
Note
We’re going to use a file-based & JSON setup. This is for simplicity in the example code & to avoid further dependencies.
This is suitable for this toy app, but isn’t recommended for production unless you know what you’re doing. GET YOU AN DATABASE!
We’ll leave the beginning code (import itty3
& app = itty3.App()
) as
well as the ending code (everything after if __name__ == "__main__":
)
alone, focusing only on our application code.
We’ll make the index view more useful first.
The Index View¶
First, let’s create a prettier index page. Alongside your app.py
file,
let’s create an index.html
file.
Add the following to that new index.html
file:
<html>
<head>
<title>My TODO List</title>
<style>
/* Just a quick reset & some basic styles. */
* { margin: 0; padding: 0; }
html { background-color: #CCCCCC; text-align: center; }
body { background-color: #FFFFFF; border-left: 2px solid #999999; border-right: 2px solid #999999; font-family: Helvetica, Arial, sans-serif; font-size: 14px; margin: 0 auto; text-align: left; width: 60%; padding: 40px 20px; }
h1, h2, h3 { margin: 10px 0px; font-family: Georgia, 'Times New Roman', Times, serif; }
p { display: block; padding: 10px 0px; }
ul { display: block; padding: 10px 0px; list-style: none; }
ul li { border: 1px solid #EEEEEE; padding: 5px; }
ul li form { display: inline; }
ul li input { margin: 0px 10px 0px 0px; }
</style>
</head>
<body>
<header>
<h1>My TODO List</h1>
</header>
<content>
<ul>
<!--
We'll manually search/replace this out with string formatting.
This is where a real template language would come in handy.
-->
{{ content }}
</ul>
<p>
<form method="post" action="/create/">
<label for="id_todo">Add TODO:</label>
<input type="text" id="id_todo" name="todo">
<input type="submit" value="Create">
</form>
</p>
</content>
</body>
</html>
Save the file & close it.
Next, alongside the app.py
& index.html
files, create a
data.json
file:
{
"items": []
}
Save the file & close it.
Now go back to app.py
& let’s update the index
view to use our
new files:
# At the top of the file, add:
import json
# ...
# Then update the ``index`` view.
@app.get("/")
def index(request):
# We'll open/read the HTML file.
with open("index.html") as index_file:
template = index_file.read()
# Pull in the JSON data (currently mostly-empty).
with open("data.json") as data_file:
data = json.load(data_file)
content = ""
# Create the list of TODO items.
for offset, item in enumerate(data.get("items", [])):
# Note: This is gross & dangerous! You need to escape your
# data in a real app to prevent XSS attacks!
content += "<li>{}</li>".format(item)
if not content:
content = "<li>Nothing to do!</li>"
# Now "template" in the data.
template = template.replace("{{ content }}", content)
# Return the response.
return app.render(request, template)
Restart the server & check http://127.0.0.1:8000/ in your browser. You should now have some HTML & an empty TODO list!
You can verify things are working by manually changing your data.json
to:
{
"items": [
"Finish TODO list app",
"Do some gardening",
"Take a nap"
]
}
Then reloading the page (no server restart needed).
Creating New TODOs View¶
Now that we can see our TODO list, let’s add a way to create new TODO. We’ll be creating a second function:
@app.post("/create/")
def create_todo(request):
# Pull in the JSON data (currently mostly-empty).
with open("data.json") as data_file:
data = json.load(data_file)
# Retrieve the new TODO text from the POST'ed data.
new_todo = request.POST.get("todo", "---").strip()
# Append it onto the TODO items.
data["items"].append(new_todo)
# Write the data back out.
with open("data.json", "w") as data_file:
json.dump(data, data_file, indent=4)
# Finally, redirect back to the main list.
return app.redirect(request, "/")
We’re doing a couple new things here. First, we’re using @app.post(...)
,
which hooks up a route for an HTTP POST request.
Second, we’re making use of request.POST
. This is a QueryDict
,
a dict
-like object that contains all the POST
’ed form values.
Note
If you’re handling JSON or another different request body, you
should NOT use request.POST
. Instead, use request.body
&
manually decode the contents of that string.
Finally, we’re using app.redirect(...)
, which is a convenience function
for sending an HTTP (temporary) redirect back to the main page. This
triggers a fresh load of the TODO list, including the newly added TODO.
Restart your python app.py
, reload in your browser & try creating a new
TODO item.
Aside: Auto-Reloading Server¶
By now, you may be tired of manually restarting your app.py
server.
To make things a little easier, we’ll set up Gunicorn to serve our
local traffic.
First, install gunicorn
:
$ pip install gunicorn
Next, instead of running python app.py
, we’ll run:
$ gunicorn -w 1 -t 0 --reload app:app
Now, whenever we change our app.py
file, gunicorn
will automatically
reload the code. Now it’ll always be serving the current code to our browser.
No more restarts!
Marking TODOs As Done¶
The last bit of our TODO list app is being able to mark a TODO as completed.
First, we’ll need to modify our index
view. Find the line:
content += "<li>{}</li>".format(item)
…and change it to:
content += '<li><form method="post" action="/done/{offset}/"><input type="submit" value="Complete"></form>{item}</li>'.format(
offset=offset,
item=item
)
We unfortunately need to use POST
here, as HTML forms can’t submit
DELETE
requests.
Note
Yes, there are better ways to handle this form submission. Adding some modern JS would be a good exercise for the reader. :)
Finally, we need to add a new view:
@app.post("/done/<int:offset>/")
def mark_done(request, offset):
# Pull in the JSON data (currently mostly-empty).
with open("data.json") as data_file:
data = json.load(data_file)
items = data.get("items", [])
if offset < 0 or offset >= len(items):
return app.error_404(request, "Not Found")
# Move it to "done".
data.setdefault("done", [])
data["done"].append(items[offset])
# Slice out the offset.
plus_one = offset + 1
data["items"] = items[:offset] + items[plus_one:]
# Write the data back out.
with open("data.json", "w") as data_file:
json.dump(data, data_file, indent=4)
# Finally, redirect back to the main list.
return app.redirect(request, "/")
This is very similar to the create_todo
view, with just a couple
modifications.
First, note that our path is /done/<int:offset>/
& we’ve added a
offset
parameter to the view’s function declaration. This lets us
capture data out of the URL & use it in our app.
Next, we use that offset
to check to ensure it’s within the bounds
of the items
. If not, we call app.error_404(...)
to supply a
404 Not Found
response.
We then archive the item to a (potentially new) "done"
key within our
JSON. And then we overwrite the items
with a slice that excludes the
desired offset.
Reload in the browser & give it a spin!
Congratulations!¶
With this, you’ve built your first working application with itty3
.
For more information, you can:
- Refer to the API docs at API Documentation
- Read some example code
- Check out the Deploying itty3 guide
- Find out how to Extending itty3
- Learn how to Troubleshooting
Happy developing!