Quickstart

Ready to get started? This section gives you a quick introduction to using Findig and it’s basic patterns.

The tiniest JSON application

Here’s the smallest app you can write in Findig:

from findig.json import App
from werkzeug.serving import run_simple

app = App(indent=4, autolist=True)

if __name__ == '__main__':
    run_simple('localhost', 5000, app)

This barebones app exposes a list of resources that are exposed by your app, and the HTTP methods that they support. Save it as listr.py, and run as follows:

$ python listr.py
 * Running on http://localhost:5000/ (Press CTRL+C to quit)

Now, send a GET request to the root of your app:

$ curl http://localhost:5000/
[
    {
        "methods": [
            "HEAD",
            "GET"
        ],
        "url": "/",
        "is_strict_collection": false
    }
]

We see that the response is a singleton list with an autogenerated JSON resource. That single item is the same one we just queried, and it was added by the autolist=True argument to our application. That tells the application to create a resource at the server root that lists all of the resources available on the API. Since we haven’t added any other resources, this is the only one available.

Adding resources

To add a resource, tell the app how to get the resource’s data, and what URLs route to it:

@app.route("/resource")
def resource():
    return { "msg": "Hello World!" }

What we’ve defined here is a pretty useless static resource, but it’s okay because it’s just for illustrative purposes. Typically, each resource is defined in terms of a function that gets the data associated with it, here we’ve aptly called ours resource. The app.route() decorator factory takes a URL rule specification and registers our resource at any URL that matches.

Hint

So what type should your resource function return? Well, Findig doesn’t actually have any specific restrictions. If you’re working with a JSON app though, you should probably stick to Python mappings and iterables.

Other types can by used seamlessly with custom formatters.

The code above is actually shorthand for the following:

@app.route("/resource")
@app.resource
def resource():
    return { "msg": "Hello World!" }

app.resource takes a resource function and turns it into a Resource, which can still be called as though they are resource functions.

You can use arguments to customize resource creation:

@app.route("/resource")
@app.resource(name="my-super-special-resource")
def resource():
    return {"msg": "Hello, from {}".format(resource.name)}

Resource names are unique strings that identify the resource somehow. By default, Findig will try to generate one for you, but that can be overridden if you want your resources to follow a particular naming scheme (or if you want to treat two resources as the same, by giving them the same name).

See finding.resource.Resource for a full listing of arguments that you can pass to resources.

Note

While you can omit @app.resource if your resource doesn’t need any arguments, you shouldn’t ever omit @app.route unless you really intend to have a resource that can never be reached by the outside world!

Collections

Some resources can be designated as collections of other resources. Resource instances have a special decorator to help you set this up:

@app.route("/people/<int:idx>")
def person(idx):
    return people()[idx-1]

@app.route("/people/")
@person.collection
def people():
    return [
        {"name": "John Doe", "age": 40},
        {"name": "Jane Smith", "age": 34}
    ]

What’s going on here? Well, we’ve defined a person resource that routes to a strange looking URL. Actually, /people/<int:idx> is a URL rule specification; any URL that matches it will route to this resource. The angle brackets indicate a variable part of the URL. It includes an optional converter, and the name of the variable part. This spec will match the URLs /people/0, /people/1, people/2 etc, but not /people/anthony (because we specfied an int converter; to match ordinary strings, just omit the converter: people/<idx>). A URL spec can have as many variable parts as needed, however the resource function must take a named parameter matching each of the variable parts.

Next, we define a people resource that’s a collection of person using person.collection as a decorator. The resource functions for collections are expected to return iterables that contain representations of the contained items.

Like resources, collections can take arguments too:

@app.route("/people/")
@person.collection(include_urls=True)
def people():
    return [
        {"name": "John Doe", "age": 40, "idx": 1},
        {"name": "Jane Smith", "age": 34, "idx": 2}
    ]

The include_urls=True instructs the collection to insert a URL field in the generated items that points to that specific item on the API server. The only caveat is that the item data that we return from the collection’s resource function has to have enough information contained to build a URL for the item (that’s why we added the idx field here).

See finding.resource.Collection for a full listing of arguments that you can pass to collections.

Data operations

The HTTP methods that Findig will expose depends on the data operations that you’ve defined for your resource. By default, GET operations are exposed for every resource, since we have to define resource functions that get the resources’s data. Makes sense right?

But what about the other HTTP methods? We can support PUT requests by telling Findig how to write new resource data:

@resource.model("write")
def write_new_data(data):
    # Er, we don't have a database set up, so let's just complain
    # that it's poorly formatted.
    from werkzeug.exceptions import BadRequest
    raise BadRequest

That .model() function is actually a function available by default on Resource instances that lets you provide functions that manipulate resource data. Here, we’re specifying a function that writes new data for the resource, and its only argument is the new data that should be written, taken from the request body. Here’s a complete list of data operations that you can add, and what they should do:

Operation Arguments Description
write data Replaces completely the data for the resource, and enables PUT requests on the resource.
make data Creates a new child resource with the input data. It should return a mapping of values that can be used to route to the resource. If present, it enables POST requests.
delete   Delete’s the resource’s data and enables DELETE requests on the resource.

Restricting HTTP Methods

Sometimes, your might define more data operations for a resource than you want directly exposed on the API. You can restrict the HTTP methods for a resource through it’s route:

@app.route("/resource", methods=['GET', 'DELETE'])
def resource():
    # return some resource data
    pass

@resource.model('write')
def write_resource(data):
    # save the resource data
    pass

@resource.model('delete')
def delete_resource():
    # delete the resource
    pass

PUT requests to this resource will fail with status 405: METHOD NOT ALLOWED, even though we have a write operation defined.

Custom applications

Suppose you wanted to build an API that wasn’t JSON (hey, I’m not here to judge)? That’s entirely possible. You just have to tell Findig how to convert to and from the content-types that you plan to use.

from findig import App

app = App()

@app.formatter.register("text/xml")
def convert_to_xml(data):
    s = to_xml(data)
    return s # always return a string

@app.parser.register("text/xml")
def convert_from_xml(s)
    obj = from_xml(s)
    return obj

Pretty straightforward stuff; convert_to_xml is a function that takes resource data and converts it to an xml string. We register it as the data formatter for the text/xml content-type using the @app.formatter.register("text/xml") decorator. Whenever a client sends an Accept header with the text/xml content-type, this formatter will be used. Similarly, convert_from_xml converts an xml string to resource data, and is called when a request with a text/xml content-type is received.

That’s great, but what happens if the client doesn’t send an Accept header, or if it sends request content without a content-type? Well, Findig will send a text/plain response (it calls str on the resource data; hardly elegant) in the first case, and send back an UNSUPPORTED MEDIA TYPE error in the second case. To avoid this, you can set a default content-type that is assumed if the client doesn’t specify one. Here’s the same example from above setting text/xml as the default:

from findig import App

app = App()

@app.formatter.register("text/xml", default=True)
def convert_to_xml(data):
    s = to_xml(data)
    return s # always return a string

@app.parser.register("text/xml", default=True)
def convert_from_xml(s)
    obj = from_xml(s)
    return obj

Tip

findig.json.App does this for the application/json content-type.

An application can register as many parsers and formatters as it needs, and can even register them on specific resources. Here’s how:

from pickle import dumps
from findig import App

app = App()
app.formatter.register("x-application/python-pickle", dumps, default=True)

@app.route("/my-resource")
def resource():
    return {
        "name": "Jon",
        "age": 23,
    }

@resource.formatter.register("text/xml")
def format_resource(data):
    return "<resource><name>{}</name><age>{}</age></resource>".format(
        data['name'],
        data['age']
    )

So this app has a global formatter that pickles resources and returns them to the client (look, it’s just an example, okay?). However, it has a special resource that can handle text/xml responses as well, using the resource-specific formatter that we defined.