itty3 Contents

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:

Happy developing!

Extending itty3

itty3 is designed to be streamlined & simple to use out of the box. However, it’s relatively easy to customize as well.

Custom HttpResponse

In normal usage, app.render(...) is simply a shortcut/convenience method for constructing an itty3.HttpResponse object. There’s nothing extraordinary about these instances, so you can make them yourself if you prefer:

import pyyaml

@app.get("/whatev/")
def whatev(request):
    data = {
        "greeting": "Hello",
        "noun": "world",
        "punctuation": "!",
    }
    return itty3.HttpResponse(
        body=pyyaml.dump(data)
        content_type="text/yaml"
    )

Or even subclass it to “bake in” complex behavior:

class YAMLResponse(itty3.HttpResponse):
    def __init__(self, data, **kwargs):
        body = pyyaml.dump(data)
        kwargs["content_type"] = "text/yaml"
        super().__init__(body, **kwargs)


@app.get("/whatev/")
def whatev(request):
    data = {
        "greeting": "Hello",
        "noun": "world",
        "punctuation": "!",
    }
    return YAMLResponse(data)

And you can override/extend App.render if you want to automatically use your new subclass:

class MyApp(itty3.App):
    def render(self, request, *args, **kwargs):
        # Because of the mismatch in signatures (`body` vs `data`), this
        # may not work perfectly in all situations.
        return YAMLResponse(*args, **kwargs)

Custom HttpRequest

As with HttpResponse, itty3.HttpRequest isn’t particularly special. The only interesting/tricky part is automatically constructed by App.create_request, so you’ll need to override that method when creating a subclass.

For instance, if you wanted to identify a request was secure in a different way (e.g. verifying a nonce was present in headers):

class NonceHttpRequest(itty3.HttpRequest):
    def is_secure(self):
        if "X-Secure-Nonce" not in self.headers:
            return False

        nonce = self.headers["X-Secure-Nonce"]
        # Check a DB for that nonce within a time range.

        if not verified:
            return False

        # The request is good. Carry on.
        return True

You could then tell your App to always use this subclass with:

class SuperSecureApp(itty3.App):
    def create_request(self, environ):
        return NonceHttpRequest.from_wsgi(environ)

Different Routing

If itty3’s routing doesn’t suit you, you can even define your own variant of Route. As long as it conforms to a bit of expected API & with a couple minor tweaks to App…:

# We want to support routes like:
#
#     "/app/:id/:title"
#
# ...and attempt automatic conversion of types.
# This is a naive implementation of that.

class SimpleRoute(object):
    def __init__(self, method, path, func):
        self.method = method
        self.path = path
        self.func = func

    def split_uri(self, path):
        return path.split("/")

    def can_handle(self, method, path):
        if not self.method == method:
            return False

        internal_path_bits = self.split_uri(self.path)
        external_path_bits = self.split_uri(path)

        if len(internal_path_bits) != len(external_path_bits):
            # Without even iterating, we know it's not right.
            return False

        matched = True

        for offset, bit in enumerate(internal_path_bits):
            if not bit.startswith(":"):
                # We're looking for a non-variable, exact match.
                if bit != external_path_bits[offset]:
                    matched = False
                    break
            else:
                # It's a variable. Carry on.
                continue

        return matched

    def extract_kwargs(self, path):
        # This only gets called if the route can handle the URI.
        # So we'll take a shortcut or two here for brevity.
        internal_path_bits = self.split_uri(self.path)
        external_path_bits = self.split_uri(path)

        matches = {}

        for offset, bit in enumerate(internal_path_bits):
            if not bit.startswith(":"):
                # It's not a variable, we don't care.
                continue

            # It's a variable. Slice off the colon.
            var_name = bit[1:]

            # Extract it from the actual URI.
            value = external_path_bits[offset]

            # Try to convert the type.
            try:
                value = int(value)
            except ValueError:
                pass

            try:
                value = float(value)
            except ValueError:
                pass

            if value in ("true", "false"):
                # Store it as a boolean.
                value = value == "true"

            # Finally, track what we found.
            matches[var_name] = value

        return matches


# Now, just override ``App`` to use your new routes.
class SimpleRoutesApp(itty3.App):
    def add_route(self, method, path, func):
        # We swap in the custom class here.
        route = SimpleRoute(method, path, func)
        self._routes.append(route)

Deploying itty3

Deployment Checklist

  • Ensure App.debug is set to False
  • Use a proper WSGI server
  • Check your 404 & 500 pages to ensure no data is leaking
  • Set up static asset serving

Managing App.debug

A solid way to manage the App.debug option is to rely on environment-based configuration.:

import os

import itty3

# Rely on environment variables to set `debug`.
app = itty3.App(
    debug=bool(os.environ.get("APP_DEBUG", False))
)

This allows your code to automatically deploy to new environments (such as production) with debug=False. For local development, you can export the environment variable to turn debug back on.:

$ export APP_DEBUG=1
$ python app.py

Use A Proper WSGI Server

While the built-in wsgiref server is convenient, it’s not particularly feature-rich or configurable. Some good production-ready alternatives include:

All are good options, boasting good speed/compliance/configuration across the board. You can’t really go wrong, so experiment & find the one you like best.

The author has the most experience with Gunicorn, so example configuration to accommodate that follows:

# gunicorn.conf
import multiprocessing

bind = "127.0.0.1:8000"
workers = multiprocessing.cpu_count() * 2 + 1
daemon = True
reload = False
max_requests = 1000
user = "<a-configured-non-root-user>"
# group = "<an-optional-group>"

Then running the daemon with:

$ gunicorn -c gunicorn.conf myapp:app

It’s important to note that this runs gunicorn still only on the localhost. It’s generally advised to put a reverse proxy or similar in front of your WSGI workers.

As an example, here’s an nginx setup for proxying to Gunicorn:

server {
    listen 80;
    server_name example.org;
    access_log  /var/log/nginx/access.log;

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

Troubleshooting

“My server isn’t responding to traffic!”

This is commonly because of the host address itty3 is bound to. By default, the code & docs default to 127.0.0.1 (“localhost”). This prevents your application server from responding to requests from the outside world.

You’ll likely need to change the address to something like 0.0.0.0 (“respond to requests from any host”) or have a server that sits in front of your application server (like Nginx or similar).

A way to check if the application server is responding is to be on the server & issue a curl command to http://127.0.0.1:<port-goes-here>/. If it responds as expected, it’s likely an address (or port) issue.

“I’m getting a 500 error and don’t know what’s wrong!”

When App.debug=False (the default), itty3 suppresses exceptions & simply returns a static 500 error page.

You’ll want to set App.debug to True, either via:

  • the initialization - itty3.App(debug=True)
  • setting the attribute on the App itself - app.debug = True
  • the app.run(debug=True) if you’re using that

When you do this, a traceback will be provided in the console output for the server. You can use this to debug your issue.

“The traceback of my error isn’t enough!”

Other frameworks provide more comprehensive or even interactive debug pages. While nice, these come at a complexity & security cost.

Being minimalist, the next best way to figure out what’s wrong in your itty3 application is through the use of an interactive debugger, such as pdb.

You can drop a import pdb; pdb.set_trace() pretty much anywhere in your code, step through instructions & examine variable contents.

There are other options, such as wdb, pudb or even in-editor interactive debuggers! Experiment & find one you like.

Experience with an interactive debugger is an invaluable skillset to have & well worth the time you invest into it.

API Documentation

itty3

The itty-bitty Python web framework… Now Rewritten For Python 3!

class itty3.App(debug=False)

Bases: object

An orchestration object that handles routing & request processing.

Parameters:debug (bool) – Allows for controlling a debugging mode.
add_route(method, path, func)

Adds a given HTTP method, URI path & view to the routing.

Parameters:
  • method (str) – The HTTP method to handle
  • path (str) – The URI path to handle
  • func (callable) – The view function to process a matching request
create_request(environ)

Given a WSGI environment, creates a HttpRequest object.

Parameters:environ (dict-alike) – The environment data coming from the WSGI server, including request information.
Returns:A built request object
Return type:HttpRequest
delete(path)

A convenience decorator for adding a view that processes a DELETE request to routing.

Example:

app = itty3.App()

@app.delete("/blog/<slug:post_slug>/")
def delete_post(request, post_slug):
    ...
Parameters:path (str) – The URI path to handle
error_404(request)

Generates a 404 page for when something isn’t found.

Exposed to allow for custom 404 pages. Care should be taken when overriding this function, as it is used internally by the routing & Python errors bubbling from within can break the server.

Parameters:request (HttpRequest) – The request being handled
Returns:The populated response object
Return type:HttpResponse
error_500(request)

Generates a 500 page for when something is broken.

Exposed to allow for custom 500 pages. Care should be taken when overriding this function, as it is used internally by the routing & Python errors bubbling from within can break the server.

Parameters:request (HttpRequest) – The request being handled
Returns:The populated response object
Return type:HttpResponse
find_route(method, path)

Determines the routing offset for a given HTTP method & URI path.

Parameters:
  • method (str) – The HTTP method to handle
  • path (str) – The URI path to handle
Returns:

The offset of the matching route

Return type:

int

Raises:

RouteNotFound – If a matching route is not found

get(path)

A convenience decorator for adding a view that processes a GET request to routing.

Example:

app = itty3.App()

@app.get("/blog/<slug:post_slug>/")
def post_detail(request, post_slug):
    ...
Parameters:path (str) – The URI path to handle
get_log()

Returns a logging.Logger instance.

By default, we return the itty3 module-level logger. Users are free to override this to meet their needs.

Returns:The module-level logger
Return type:logging.Logger
patch(path)

A convenience decorator for adding a view that processes a PATCH (partial update) request to routing.

Example:

app = itty3.App()

@app.patch("/blog/bulk/")
def bulk_post(request):
    ...
Parameters:path (str) – The URI path to handle
post(path)

A convenience decorator for adding a view that processes a POST request to routing.

Example:

app = itty3.App()

@app.post("/blog/create/")
def create_post(request):
    ...
Parameters:path (str) – The URI path to handle
process_request(environ, start_response)

Processes a specific WSGI request.

This kicks off routing & attempts to find a route matching the requested HTTP method & URI path.

If found, the view associated with the route is called, optionally with the parameters from the URI. The resulting HttpResponse then performs the actions to write the response to the server.

If not found, App.error_404 is called to produce a 404 page.

If an unhandled exception occurs, App.error_500 is called to produce a 500 page.

Parameters:
  • environ (dict-alike) – The environment data coming from the WSGI server, including request information.
  • start_response (callable) – The function/callable to execute when beginning a response.
Returns:

The body iterable for the WSGI server

Return type:

iterable

put(path)

A convenience decorator for adding a view that processes a PUT request to routing.

Example:

app = itty3.App()

@app.put("/blog/<slug:post_slug>/")
def update_post(request, post_slug):
    ...
Parameters:path (str) – The URI path to handle
redirect(request, url, permanent=False)

A convenience function for supplying a HTTP redirect.

Parameters:
  • request (HttpRequest) – The request being handled
  • url (str) – A path or full URL to redirect the user to
  • permanent (bool, Optional) – Whether the redirect should be considered permanent or not. Defaults to False (temporary redirect).
Returns:

The populated response object

Return type:

HttpResponse

remove_route(method, path)

Removes a route from the routing.

Parameters:
  • method (str) – The HTTP method to handle
  • path (str) – The URI path to handle
render(request, body, status_code=200, content_type='text/html', headers=None)

A convenience method for creating a HttpResponse object.

Parameters:
  • request (HttpRequest) – The request being handled
  • body (str) – The body of the response
  • status_code (int, Optional) – The HTTP status to return. Defaults to 200.
  • content_type (str, Optional) – The Content-Type header to return with the response. Defaults to text/html.
  • headers (dict, Optional) – The HTTP headers to include on the response. Defaults to empty headers.
Returns:

The populated response object

Return type:

HttpResponse

render_json(request, data, status_code=200, content_type='application/json', headers=None)

A convenience method for creating a JSON HttpResponse object.

Parameters:
  • request (HttpRequest) – The request being handled
  • data (dict/list) – The Python data structure to be encoded as JSON
  • status_code (int, Optional) – The HTTP status to return. Defaults to 200.
  • content_type (str, Optional) – The Content-Type header to return with the response. Defaults to text/html.
  • headers (dict, Optional) – The HTTP headers to include on the response. Defaults to empty headers.
Returns:

The populated response object

Return type:

HttpResponse

render_static(request, asset_path)

Handles serving static assets.

WARNING: This should really only be used in development!

It’s slow (compared to nginx, Apache or whatever), it hasn’t gone through any security assessments (which means potential security holes), it’s imperfect w/ regard to mimetypes.

Parameters:
  • request (HttpRequest) – The request being handled
  • asset_path (str) – A path of the asset to be served
Returns:

The populated response object

Return type:

HttpResponse

reset_logging(level=20)

A method for controlling how App.run does logging.

Disables wsgiref’s default “logging” to stderr & replaces it with itty3-specific logging.

Parameters:level (int, Optional) – The logging.LEVEL you’d like to have output. Default is logging.INFO.
Returns:
The handler class to be used.
Defaults to a custom NoStdErrHandler class.
Return type:wsgiref.WSGIRequestHandler
run(addr='127.0.0.1', port=8000, debug=None, static_url_path=None, static_root=None)

An included development/debugging server for running the App itself.

Runs indefinitely. Use Ctrl+C or a similar process killing method to exit the server.

Parameters:
  • addr (str, Optional) – The address to bind to. Defaults to 127.0.0.1.
  • port (int, Optional) – The port to bind to. Defaults to 8000.
  • debug (bool, Optional) – Whether the server should be run in a debugging mode. If provided, this overrides the App.debug set during initialization.
  • static_url_path (str, Optional) – The desired URL prefix for static assets. e.g. /static/. Defaults to None (no static serving).
  • static_root (str, Optional) – The filesystem path to the static assets. e.g. ../static_assets. Can be either a relative or absolute path. Defaults to None (no static serving).
class itty3.HttpRequest(uri, method, headers=None, body='', scheme='http', host='', port=80, content_length=0, request_protocol='HTTP/1.0', cookies=None)

Bases: object

A request object, representing all the portions of the HTTP request.

Parameters:
  • uri (str) – The URI being requested.
  • method (str) – The HTTP method (“GET|POST|PUT|DELETE|PATCH|HEAD”)
  • headers (dict, Optional) – The received HTTP headers
  • body (str, Optional) – The body of the HTTP request
  • scheme (str, Optional) – The HTTP scheme (“http|https”)
  • host (str, Optional) – The hostname of the request
  • port (int, Optional) – The port of the request
  • content_length (int, Optional) – The length of the body of the request
  • request_protocol (str, Optional) – The protocol of the request
  • cookies (http.cookies.SimpleCookie, Optional) – The cookies sent as part of the request.
GET

Returns a QueryDict of the GET parameters.

POST

Returns a QueryDict of the POST parameters from the request body.

Useless if the body isn’t form-encoded data, like JSON bodies.

PUT

Returns a QueryDict of the PUT parameters from the request body.

Useless if the body isn’t form-encoded data, like JSON bodies.

content_type()

Returns the received Content-Type header.

Returns:The content-type header or “text/html” if it was absent.
Return type:str
classmethod from_wsgi(environ)

Builds a new HttpRequest from the provided WSGI environ.

Parameters:environ (dict) – The bag of YOLO that is the WSGI environment
Returns:
A fleshed out request object, based on what was
present.
Return type:HttpRequest
get_status_line()
is_ajax()

Identifies if the request came from an AJAX call.

Returns:True if sent via AJAX, False otherwise
Return type:bool
is_secure()

Identifies whether or not the request was secure.

Returns:True if the environment specified HTTPs, False otherwise
Return type:bool
json()

Decodes a JSON body if present.

Returns:The data
Return type:dict
split_uri(full_uri)

Breaks a URI down into components.

Parameters:full_uri (str) – The URI to parse
Returns:
A dictionary of the components. Includes path, query
fragment, as well as netloc if host/port information is present.
Return type:dict
class itty3.HttpResponse(body='', status_code=200, headers=None, content_type='text/plain')

Bases: object

A response object, to make responding to requests easier.

A lightly-internal start_response attribute must be manually set on the response object when in a WSGI environment in order to send the response.

Parameters:
  • body (str, Optional) – The body of the response. Defaults to “”.
  • status_code (int, Optional) – The HTTP status code (without the reason). Default is 200.
  • headers (dict, Optional) – The headers to supply with the response. Default is empty headers.
  • content_type (str, Optional) – The content-type of the response. Default is text/plain.

Removes a cookie.

Succeed regards if the cookie is already set or not.

Parameters:
  • key (str) – The name of the cookie.
  • path (str, Optional) – The path the cookie is valid for. Default is “/”.
  • domain (str, Optional) – The domain the cookie is valid for. Default is None (only the domain that set it).

Sets a cookie on the response.

Takes the same parameters as the http.cookies.Morsel object from the Python stdlib.

Parameters:
  • key (str) – The name of the cookie.
  • value (Any) – The value of the cookie.
  • max_age (int, Optional) – How many seconds the cookie lives for. Default is None (expires at the end of the browser session).
  • expires (str or datetime, Optional) – A specific date/time (in UTC) when the cookie should expire. Default is None (expires at the end of the browser session).
  • path (str, Optional) – The path the cookie is valid for. Default is “/”.
  • domain (str, Optional) – The domain the cookie is valid for. Default is None (only the domain that set it).
  • secure (bool, Optional) – If the cookie should only be served by HTTPS. Default is False.
  • httponly (bool, Optional) – If True, prevents the cookie from being provided to Javascript requests. Default is False.
  • samesite (str, Optional) – How the cookie should behave under cross-site requests. Options are itty3.SAME_SITE_NONE, itty3.SAME_SITE_LAX, and itty3.SAME_SITE_STRICT. Default is None. Only for Python 3.8+.
set_header(name, value)

Sets a header on the response.

If the Content-Type header is provided, this also updates the value of HttpResponse.content_type.

Parameters:
  • name (str) – The name of the header.
  • value (Any) – The value of the header.
write()

Begins the transmission of the response.

The lightly-internal start_response attribute MUST be manually set on the object BEFORE calling this method! This callable is called during execution to set the status line & headers of the response.

Returns:An iterable of the content
Return type:iterable
Raises:ResponseFailed – If no start_response was set before calling.
exception itty3.IttyException

Bases: Exception

The base exception for all itty3 exceptions.

class itty3.QueryDict(data=None)

Bases: object

Simulates a dict-like object for query parameters.

Because HTTP allows for query strings to provide the same name for a parameter more than once, this object smoothes over the day-to-day usage of those queries.

You can act like it’s a plain dict if you only need a single value.

If you need all the values, QueryDict.getlist & QueryDict.setlist are available to expose the full list.

get(name, default=None)

Tries to fetch a value for a given name.

If not found, this returns the provided default.

Parameters:
  • name (str) – The name of the parameter you’d like to fetch
  • default (bool, defaults to None) – The value to return if the name isn’t found.
Returns:

The found value for the name, or the default.

Return type:

Any

getlist(name)

Tries to fetch all values for a given name.

Parameters:name (str) – The name of the parameter you’d like to fetch
Returns:The found values for the name.
Return type:list
Raises:KeyError – If the name isn’t found
items()

Returns all the parameter names & values.

Returns:
A list of two-tuples. The parameter names & the first
value for that name.
Return type:list
keys()

Returns all the parameter names.

Returns:A list of all the parameter names
Return type:list
setlist(name, values)

Sets all values for a given name.

Parameters:
  • name (str) – The name of the parameter you’d like to fetch
  • values (list) – The list of all values
Returns:

None

exception itty3.ResponseFailed

Bases: itty3.IttyException

Raised when a response could not be returned to the server/user.

class itty3.Route(method, path, func)

Bases: object

Handles setting up a given route. Composed of a HTTP method, a URI path & “view” (a callable function that takes a request & returns a response object) for handling a matching request.

Variables can be added to the path using a <type:variable_name> syntax. For instance, if you wanted to capture a UUID & an integer in a URI, you could provide the following path:

"/app/<uuid:app_id>/version/<int:major_version>/"

These would be added onto the call to the view as additional arguments. In the case of the previous path, the view’s signature should look like:

def app_info(request, app_id, major_version): ...

Supported types include:

  • str
  • int
  • float
  • slug
  • uuid
Parameters:
  • method (str) – The HTTP method
  • path (str) – The URI path to match against
  • func (callable) – The view function to handle a matching request
can_handle(method, path)

Determines if the route can handle a specific request.

Parameters:
  • method (str) – The HTTP method coming from the request
  • path (str) – The URI path coming from the request
Returns:

True if this route can handle the request, False otherwise

Return type:

bool

convert_types(matches)

Takes raw matched from requested URI path & converts the data to their proper type.

Parameters:matches (dict) – The variable names & the string data found for them from the URI path.
Returns:The converted data
Return type:dict
create_re(path)

Creates a compiled regular expression of a path.

It’d be unusual to need this as an end-user, but who am I to stop you? :)

Parameters:path (str) – A URI path, potentially with <type:variable_name> bits in it.
Returns:
A tuple of the compiled regular expression that suits the
path & dict of the variable names/type conversions to be done upon matching.
Return type:tuple
extract_kwargs(path)

Pulls variables out of the requested URI path.

Parameters:path (str) – The URI path coming from the request
Returns:
A dictionary of the variable names from the path &
converted data found for them. Empty dict if no variables were present.
Return type:dict
get_re_for_any()
get_re_for_float()
get_re_for_int()
get_re_for_slug()
get_re_for_type(desired_type)

Fetches the correct regex for a given type.

Parameters:desired_type (str) – The provided type to get a regex for
Returns:A raw string of the regex (minus the variable name)
Return type:str
get_re_for_uuid()
known_types = ['str', 'int', 'float', 'uuid', 'slug']
exception itty3.RouteNotFound

Bases: itty3.IttyException

Raised when no method/path combination could be found.

itty3.get_version(full=False)

Fetches the current version of itty3.

Parameters:full (bool) – Chooses between the short semver version and the longer/full version, including release information.
Returns:The version string
Return type:str

Philosophy

The following are the guiding principles around itty3:

What itty3 Is

  • Small (ideally, ~1kloc of code or less)
  • Self-contained (depends only on the Python stdlib)
  • Open (BSD license, baby!)
  • Respects HTTP & the Web (all the HTTP verbs, good status codes,
    content-types, etc.)
  • Easy to start working with (import itty3 & define some functions)
  • Flexible & easy to extend (all code can be used in relative isolation)
  • Few to no globals (outside of constants)
  • Fast & efficient (within reason/readability)
  • Well-tested
  • Well-documented
  • Unicode everywhere (that we can)

itty3 is designed for the sweet spot around creating single-page apps, small APIs & experiments. The code you produce with itty3 should be (relatively) easy to migrate to other things (Django, Flask, etc.) when you’ve moved beyond itty3’s capabilities.

What itty3 Isn’t

  • An everything-and-the-kitchen-sink webframework (there are better options)
  • Strongly opinionated about tools
    (BYO-database-layer-template-engine-Javascript-framework-etc)
  • A perfect solution

itty3 won’t ever ship with an authentication layer, database engines, scaffolding, Makefiles (beyond the Sphinx one), etc.

It’s designed for the modern Web, so I’m sure there’s ancient things that don’t work. Sorrynotsorry.

The Future

There are planned improvements for the future. The Github Issues for the project is the most up-to-date source of that information, but generally speaking:

  • Cookie support
  • Included example code
  • More/better docs
  • Maybe file uploads? Maybe

Contributing

itty3 gladly accepts contributions that:

  • are respectful to everyone
  • inclusive by default (we’re all human)
  • report/fix Real World™ problems

If this list offends you or you feel you can’t follow these guidelines, you’re welcome to use other frameworks or to not contribute.

Ways To Contribute

In easy-to-harder order:

  • File an issue/bug report
  • Add/update documentation
  • Add test coverage
  • Add feature code

File An Issue/Bug Report

If you encounter a bug or a shortcoming in itty3, one of the easiest ways to help is to create a GitHub Issue.

Simply click the link and fill out the template.

The more detail you can provide, the easier it will be to reproduce your issue & get it resolved.

Thanks for the help!

Add/Update Documentation

Documentation is crucial in understanding & using software libraries. And itty3’s documentation is no exception.

That said, mistakes & omissions happen. Helping fix the documentation is an easy way to help everyone!

To submit a documentation addition/update:

  • Fork the repository on GitHub
  • Clone your fork to your local machine (or edit in the GitHub UI)
  • Create a new branch with git
  • Change the documentation in question (located in the docs/ folder)
  • Add your changes via git
  • Commit the changes to your branch
  • Push the branch back to GitHub
  • Open a Pull Request for your branch
  • Fill out the template & submit!

With apologies to Edgar Allen Poe,… Quoth the raven: Documentation forevermore!

Add Test Coverage

All software has bugs. Tests prove that the code works as intended.

To submit a test addition/update:

  • Fork the repository on GitHub
  • Clone your fork to your local machine (or edit in the GitHub UI)
  • Create a new branch with git
  • Create a virtualenv/pipenv for the repository & activate it
  • Run $ pip install pytest
  • Run $ pytest tests & ensure they’re passing.
  • Make your changes/additions to test files.
  • Run $ pytest tests again & ensure your changes pass.
  • Add your changes via git
  • Commit the changes to your branch
  • Push the branch back to GitHub
  • Open a Pull Request for your branch
  • Fill out the template & submit!

I :heart: more tests, always.

Add Feature Code

Most everyone loves new features. As long as new features fall within the Philosophy of itty3, they’re welcomed & appreciated!

To submit a new feature:

  • Fork the repository on GitHub
  • Clone your fork to your local machine (or edit in the GitHub UI)
  • Create a new branch with git
  • Create a virtualenv/pipenv for the repository & activate it
  • Run $ pip install pytest
  • Run $ pytest tests & ensure they’re passing.
  • Add your feature work.
  • Ensure the new feature has a docstring if it’s public API.
  • Ensure the new feature matches existing code style (black is nice here).
  • Add documentation to the main guides as appropriate.
  • Run $ pytest tests again & ensure your changes pass.
  • Add your changes via git
  • Commit the changes to your branch
  • Push the branch back to GitHub
  • Open a Pull Request for your branch
  • Fill out the template & submit!

You da bes!