.. _tutorial: ======== 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 : 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:: My TODO List

My TODO List

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 += "
  • {}
  • ".format(item) if not content: content = "
  • Nothing to do!
  • " # 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! .. _Gunicorn: https://gunicorn.org/ 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 += "
  • {}
  • ".format(item) ...and change it to:: content += '
  • {item}
  • '.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//") 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//`` & 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 :doc:`reference/itty3` * Read some `example code`_ * Check out the :ref:`deploying` guide * Find out how to :ref:`extending` * Learn how to :ref:`troubleshooting` Happy developing! .. _`example code`: https://github.com/toastdriven/itty3/tree/master/examples