.. _quickstart: 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 :py:meth:`app.route() <findig.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!" } :py:meth:`app.resource <findig.App.resource>` takes a resource function and turns it into a :py:class:`~findig.resource.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 :py:class:`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 :py:class:`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 :py:class:`~findig.resource.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: 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. .. code-block:: python 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: .. code-block:: python 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:: :py:class:`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.