The Tutorial ============ .. note:: This tutorial assumes some familiarity with the Falcon_ framework so make sure you check it out if you haven't already. Installation ------------ Using PIP ^^^^^^^^^ ``pip`` supports installing packages from git repositories: .. code-block:: bash pip install git+ssh://git@bitbucket.org/leadbrite/req.git@v1.14.0#egg=req This works inside of requirements files as well: .. code-block:: bash echo "git+ssh://git@bitbucket.org/leadbrite/req.git@v1.14.0#egg=req" >> requirements.txt From Source ^^^^^^^^^^^ You can vendor `req` by copying it into your project. If you go this route make sure that python-dateutil_ is on your ``PYTHONPATH``. Goals of the Tutorial --------------------- This tutorial will guide you through building a simple blogging application's API. By the end of this tutorial you will have a sufficient understanding of req to be able to start using it in your own projects. Structure Your Project ---------------------- While Falcon and req don't impose any particular project structure, for the purposes of this tutorial we will structure our project as follows: .. code-block:: bash example/ ├── blog │   ├── __init__.py │   ├── endpoints.py # contains the req Endpoint declarations │   ├── handlers.py # contains the Falcon handlers │   ├── models.py # contains the database interfaces │   └── schema.py # contains the req Schema declarations └── main.py # initializes Falcon and registers the Endpoints .. note:: Since req does not provide any facilities for interacting with database management systems, we will not focus on the contents of the `models.py` file. That part is left as an exercise to the reader to implement using their preferred storage engine. Core Concepts ------------- Before we begin, let's briefly outline the two major concepts in req: Schemas Schemas describe how data is structured. You normally use them to validate user inputs but they're also usable as containers for data (like models in an ORM) and they can be adapted to various data definition formats like JSONSchema_. Endpoints Endpoints describe how data can be reached. They expose a domain-specific language for hooking up your routes, schema and handlers. A Schema for Articles --------------------- The API is going to expose endpoints for creating, reading and listing blog Articles, so start by defining how an Article is supposed to be structured: .. code-block:: python :caption: example/blog/schema.py :linenos: from req import schema class Article(schema.Schema): headline = schema.String(_max_length=150) content = schema.String() author_email = schema.Email(_name="authorEmail") With that, you get a simple Python interface with which you can construct, render and validate Article data:: >>> from blog.schema import Article You can construct Articles:: >>> article = Article( ... headline="Hello, World!", ... content="...", ... author_email="bogdan@ave81.com" ... ) Convert them to dictionaries:: >>> article.materialize() { "headline": "Hello, World!", "content": "...", "authorEmail": "bogdan@ave81.com" } Use them to validate inputs:: >>> data = article.materialize() >>> article.validate(data) is None True :: >>> article.validate({}) Traceback (most recent call last): ... ValidationError: ["headline: missing", "content: missing", "authorEmail: missing"] :: >>> article.validate({"authorEmail": "bogdan"}) Traceback (most recent call last): ... ValidationError: ["headline: missing", "content: missing", "authorEmail: invalid e-mail 'bogdan'"] You can populate their individual properties with data:: >>> article = Article() >>> article.headline = "Hello" >>> article.headline "Hello" :: >>> article.content = 42 Traceback (most recent call last): ... ValidationError: invalid string Finally, you can update all their data at once:: >>> article = Article() >>> article.update({"headline": "Hello", "content": "...", "authorEmail": "john@example.com"}) >>> article.headline "Hello" :: >>> article.update({"headline": "Hello", "authorEmail": "john@example.com"}) >>> article.content = 42 Traceback (most recent call last): ... ValidationError: ["content: missing"] Next let's define a naïve Falcon handler for adding new Articles to the system: .. literalinclude:: example/blog/handlers_naive.py :caption: example/blog/handlers.py :linenos: That's... not very pretty. In fact, the reason we called that a naïve handler is because req exposes decorators that can help you write the equivalent\ [#f1]_ handler much more concisely: .. literalinclude:: example/blog/handlers_simple.py :caption: example/blog/handlers.py :lines: 1-4, 5-13 :linenos: .. warning:: The order in which the decorators are applied to the ``on_post`` method is significant: in order for errors that occur when ``jsonrequest`` runs to be formatted correctly, it must be applied to the handler method before ``jsonresponse``. This rule holds for all request/response decorators in req. Your First Endpoints -------------------- Now that you have a working handler, you can declare an endpoint for it: .. literalinclude:: example/blog/endpoints_simple.py :caption: example/blog/endpoints.py :lines: 1-4, 6- :linenos: This probably looks a little weird. Let's look at each chunk in isolation: .. literalinclude:: example/blog/endpoints_simple.py :lines: 6-7 Declares an Endpoint called `V1` whose path is ``/blog/v1``. This will serve as the root of our API and it has no handler or methods of its own. .. literalinclude:: example/blog/endpoints_simple.py :lines: 9-11 Declares an Endpoint called `Articles` whose ``path`` is unspecified, so it will default to ``/articles``. Since this Endpoint is nested under `V1` its full path is going to be ``/blog/v1/articles``. Since its ``handler`` points to ``handlers.Articles``, when this Endpoint gets registered with Falcon its route is going to point to an instance of that class. .. literalinclude:: example/blog/endpoints_simple.py :lines: 13- This block states that the `Articles` endpoint accepts `POST` requests whose payloads comply with the ``Article`` schema and that the response payload will have the same structure. This is how ``jsonrequest`` figures out which schema to use when validating requests. Now that your endpoints are in place, you can register them with falcon: .. literalinclude:: example/main.py :caption: :linenos: And take your API for a spin: .. code-block:: bash ~/example$ gunicorn main:app [INFO] Starting gunicorn 19.3.0 [INFO] Listening at: http://127.0.0.1:8000 (20493) [INFO] Using worker: sync [INFO] Booting worker with pid: 20496 DEBUG:req.falcon:Registered endpoint 'articles' with path '/blog/v1/articles'... Req tells us it registered a single endpoint at ``/blog/v1/articles``. Try creating an article\ [#f2]_: .. code-block:: bash $ http POST http://localhost:8000/blog/v1/articles headline="Hello, World!" content="hello" authorEmail="bogdan@ave81.com" HTTP/1.1 201 Created Connection: close Date: Wed, 23 Mar 2016 14:55:20 GMT Server: gunicorn/19.3.0 content-length: 110 content-type: application/json; charset=utf-8 { "_status": { "code": 201 }, "authorEmail": "bogdan@ave81.com", "content": "hello", "headline": "Hello, World!" } .. note:: HTTPie\ [#f2]_ automatically generates a JSON payload based on the parameters we give it so the above command is the same as this curl command:: curl -H"Content-type: application/json" \ -d"{\"headline\": \"Hello, World!\", \"content\": \"hello\", \"authorEmail\": \"bogdan@ave81.com\"}" \ http://localhost:8000/blog/v1/articles It looks like that worked; try making some malformed requests: .. code-block:: bash $ http POST http://localhost:8000/blog/v1/articles HTTP/1.1 400 Bad Request Connection: close Date: Wed, 23 Mar 2016 14:47:05 GMT Server: gunicorn/19.3.0 content-length: 103 content-type: application/json; charset=utf-8 { "_status": { "code": 400, "errors": [ { "message": "missing Content-Type header", "severity": "error" } ] } } .. code-block:: bash $ http POST http://localhost:8000/blog/v1/articles content="hello" HTTP/1.1 400 Bad Request Connection: close Date: Wed, 23 Mar 2016 14:49:03 GMT Server: gunicorn/19.3.0 content-length: 151 content-type: application/json; charset=utf-8 { "_status": { "code": 400, "errors": [ { "message": "headline: missing", "severity": "error" }, { "message": "authorEmail: missing", "severity": "error" } ] } } .. code-block:: bash $ http POST http://localhost:8000/blog/v1/articles content="hello" authorEmail="bogdan" HTTP/1.1 400 Bad Request Connection: close Date: Wed, 23 Mar 2016 14:53:19 GMT Server: gunicorn/19.3.0 content-length: 167 content-type: application/json; charset=utf-8 { "_status": { "code": 400, "errors": [ { "message": "headline: missing", "severity": "error" }, { "message": "authorEmail: invalid e-mail 'bogdan'", "severity": "error" } ] } } Looking Up Individual Articles ------------------------------ You're going to want to be able to grab individual Articles by id after you create them. Unfortunately, Articles don't contain ids right now. Let's fix that. Start by splitting the Article schema into three separate classes: .. literalinclude:: example/blog/schema.py :caption: :lines: 1-15 :linenos: BaseArticle This serves as a representation for the common set of properties present on all Article requests and responses. Other Schemas can extend this and inherit its properties. CreateArticle This serves as the Schema for Article creation requests. Article This serves as the Schema for Article responses. This has one extra property compared to `BaseArticle`, the Article's numeric id. Update the `Articles` endpoint to reflect the schema change: .. literalinclude:: example/blog/endpoints.py :caption: :lines: 9-11, 22- Now if you create an Article, its response will contain an id property: .. code-block:: bash $ http POST http://localhost:8000/blog/v1/articles headline="Hello, World!" content="hello" authorEmail="bogdan@ave81.com" HTTP/1.1 201 Created Connection: close Date: Wed, 23 Mar 2016 15:30:01 GMT Server: gunicorn/19.3.0 content-length: 119 content-type: application/json; charset=utf-8 { "_status": { "code": 201 }, "authorEmail": "bogdan@ave81.com", "content": "hello", "headline": "Hello, World!", "id": 1 } .. note:: We're assuming that the ``insert_article`` method was also updated to return each Article's id. All you have to do now is add an endpoint and a handler for looking up individual Articles: .. literalinclude:: example/blog/endpoints.py :caption: :lines: 9-17, 21- :emphasize-lines: 4-9 The curly bracket syntax in the new path denotes that we expect the path to contain a dynamic parameter holding the id. That value will be passed into the handler as a keyword argument. .. literalinclude:: example/blog/handlers.py :caption: :lines: 1-4, 19-27 Restart your server and you should see the new endpoint being registered: .. code-block:: bash ~/example$ gunicorn main:app [INFO] Starting gunicorn 19.3.0 [INFO] Listening at: http://127.0.0.1:8000 (24784) [INFO] Using worker: sync [INFO] Booting worker with pid: 24787 DEBUG:req.falcon:Registered endpoint 'articles' with path '/blog/v1/articles'... DEBUG:req.falcon:Registered endpoint 'article' with path '/blog/v1/articles/{article_id}'... .. code-block:: bash $ http GET http://localhost:8000/blog/v1/articles/1 HTTP/1.1 404 Not Found Connection: close Date: Wed, 23 Mar 2016 15:48:06 GMT Server: gunicorn/19.3.0 content-length: 97 content-type: application/json; charset=utf-8 { "_status": { "code": 404, "errors": [ { "message": "article '1' not found", "severity": "error" } ] } } After creating an article: .. code-block:: bash $ http GET http://localhost:8000/blog/v1/articles/0 HTTP/1.1 200 OK Connection: close Date: Wed, 23 Mar 2016 15:53:46 GMT Server: gunicorn/19.3.0 content-length: 119 content-type: application/json; charset=utf-8 { "_status": { "code": 200 }, "authorEmail": "bogdan@ave81.com", "content": "hello", "headline": "Hello, World!", "id": 0 } Lists of Articles ----------------- Now that you can create Articles and look them up individually, you probably want to list them. Once again, start by defining a schema: .. literalinclude:: example/blog/schema.py :caption: :lines: 17- Then a handler: .. literalinclude:: example/blog/handlers.py :caption: :lines: 7-10 And hook it all up: .. literalinclude:: example/blog/endpoints.py :caption: :lines: 9- :emphasize-lines: 11,12 .. code-block:: bash $ http GET http://localhost:8000/blog/v1/articles HTTP/1.1 200 OK Connection: close Date: Wed, 23 Mar 2016 16:34:27 GMT Server: gunicorn/19.3.0 content-length: 132 content-type: application/json; charset=utf-8 { "_status": { "code": 200 }, "items": [ { "authorEmail": "bogdan@ave81.com", "content": "hello", "headline": "Hello, World!", "id": 0 } ] } Next Steps ---------- Congratulations on building your first req API! If you're interested in learning more we suggest you check out the :doc:`API reference documentation `. .. rubric:: Footnotes .. [#f1] The second version of the handler has improved error handling and better validation so it is not exactly "equivalent". .. [#f2] We're using the httpie_ utility to make requests to our API because it's simpler to use than something like curl_ but you should feel free to use whatever tools you're familiar with. You might consider using Postman_ instead of either of those if you prefer GUI tools. .. _httpie: https://github.com/jkbrzt/httpie .. _curl: https://curl.haxx.se/ .. _Postman: https://www.getpostman.com/ .. _Falcon: http://falconframework.org/ .. _python-dateutil: https://pypi.python.org/pypi/python-dateutil .. _JSONSchema: http://json-schema.org/