Skip to content

tswicegood/steinie

Repository files navigation

steinie

A little framework for doing web applications

Usage

Steinie is built around the concept of routes. Your application is made up of one or more routes that guide the web request through your code. Let's start with the simplest of simple, the Hello World web application.

from steinie import Steinie
app = Steinie()


@app.get("/")
def get(request, response):
    return "Hello World, from Steinie!\n"


if __name__ == "__main__":
    app.run()

You can run this directly using python if you save this to a file. This starts up a simple development server on port 5151 that responds to the / route on your local computer. Give it a try.

Steinie uses Werkzeug for handling its routes. This means all of your familiar route patterns are available to you inside Steinie.

Steinie has built-in decorators for GET and POST along with DELETE, HEAD, INFO, OPTIONS, PATCH, PUT, and TRACE.

Dealing with Parameters

Another common need is to provide parameters to your web application. Lets say you wanted to add a username to your path, but you wanted it capitalized (bear with me for a minute), you can do that with the param decorator like this:

from steinie import Steinie

app = Steinie()


@app.param("username")
def capitalize(param):
    return param.capitalize()


@app.get("/<username:some_user>")
def handler(request, response):
    return "Hello, %s\n" % request.params["some_user"]


if __name__ == "__main__":
    app.run()

Using the param decorator, you specify the name of the parameter you want to create, then you provide a function specifying what you want to do. You can run this example, then load http://localhost:5151/alice and it will respond with "Hello, Alice!"

If you're familiar with Flask's (and by extension, Werkzeug's) converters this might look very familiar. Again, building off of the Werkzeug base, much of what's provided here mimics what you might already be used to.

Now that you've seen the basic example, imagine instead if your created a function that loads a user object from your database and returns that. Using the params decorator, you can start to turn basic parameters from your incoming request into something that matches the way you've modeled your actual application.

Grouping routes and parameters

You might be wondering how this scales. A single file with a ton of decorated functions sound pretty unwieldy to me. Thankfully, Steinie provides a way to break up your functionality into logical parts through what it calls a Router.

Let's enhance the example above. Most of what's there relates to users, so we're going to create a new Route, then mount it to /user.

First, adjust the import statement so it looks like this:

from steinie import Steinie, Router

Next, create a new route object from the Router and adjust your two decorated functions to use that. It should look like this:

route = Router()


@route.param("username")
def capitalize(param):
    return param.capitalize()


@route.get("/<username:some_user>")
def handler(request, response):
    return "Hello, %s\n" % request.params["some_user"]

Finally, you need to modify your app object to use this your new route. You do that with the aptly named use method like this:

app.use("/user", route)

Save your work, fire up your code, then visit your server again. If you try to go to http://localhost:5151/alice again you'll get a 404. Instead, you need to add /user to the URL so it looks like this: http://localhost:5151/user/alice.

Dealing with Middleware

There's one more part to becoming an expert in Steine: Middleware. Middleware gives you a chance to modify the request or response for every incoming request.

Let's continue to build on our example above. Instead of using the param decorator, let's create a middleware that capitalizes all some_user parameters.

First, let's create the middleware. Steinie expects them to be objects that can be intantiated and provided a Router instance, then invoked via the __call__ method. That's it. Add this to your file and then you can say you've created your very own Steinie middleware:

class CapitalizeMiddleware(object):
    def __init__(self, route):
        pass

    def __call__(self, request, response, _next):
        if "some_user" in request.params:
            new = request.params["some_user"].capitalize()
            request.params["some_user"] = new
        return _next(request, response)

There's a couple of things to call out here. First, we don't need the route provided at instantiation time, so there's no need to store it. If you did, you could set that as an attribute on the class.

Next up, the __call__ method has three arguments. request and response are familiar from earlier, but _next is new. This is a function generated by Steinie that allows the middleware to control what happens when it's invoked. For our purposes here, we want to modify the some_user value by capitalizing it when it's present, then continue on. To do that, you simply return the result of _next(request, response).

The ability to control what happens here is a key part of Steinie's middleware. You can capture the return value from _next and do something with it. Use cases that jump to mind for me are a CacheMiddleware that attempts to load a request from cache and returns that if its found but will allow the request to go through if it hasn't been cached.

This simple example here is just that, pretty simple.

You're not quite finished with the middleware yet. Next you need to tell your router to use it. Enter router.use again:

route.use(CapitalizeMiddleware)

This is the same method you used to attach a router to an application, but this time there's no route (the first argument you used above) associated with it. Providing router.use with a single argument signals to Steinie that you're giving it a middleware that it should execute when dealing with all requests this router attempts to handle.

The final modification that you should make is to remove the params function and adjust your get route. When it's finished, it should look like this:

@route.get("/<some_user>")
def handler(request, response):
    return "Hello, %s\n" % request.params["some_user"]

Now, re-run your code and access it. You should get the same output, but this is a different pattern. What makes one pattern over the other better? Funny you should mention it, that's the next topic.

Middleware vs. Parameters

Both middleware and parameters can be used to acheive very similar goals, but they have distinct roles.

Middleware
These are global and have the chance to modify every incoming request. They have the full request and have the ability to circumvent the normal response. Use them when you need to modify every request that is processed for a given route or you need to do more than change part of the request URL to some other object.
Parameters
These are localized to routes that have the parameter in the request URL. Unlike middleware, all they can do is transform part of a URL into something else. They're very useful when translating an ID to an object in the database or some other similar transformation, but they won't let you change the response that Steinie returns.

Inspiration

Steinie was inspired in heavily by Express in the server-side JavaScript world. For the 4.x rewrite, Express started leaning heavily on the router based model that Steinie uses.

About

A little framework for doing web applications

Resources

License

Stars

Watchers

Forks

Packages

No packages published