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.