Quickstart Tutorial

This tutorial guides you through how you can use statham to improve your use of external data sources, via an example app which gets and displays winners of a poll from an external API.

Suppose we are using an external Polls API which adheres to the following JSON Schema:

{
    "type": "object",
    "required": ["question", "choices"],
    "properties": {
        "question": {"type": "string"},
        "choices": {
            "type": "array",
            "items": {"$ref": "#/definitions/choice"}
        }
    },
    "definitions": {
        "choice": {
            "type": "object",
            "required": ["choice_text"],
            "properties": {
                "choice_text": {
                    "type": "string", "maxLength": 200
                },
                "votes": {"type": "integer", "default": 0}
            }
        }
    }
}

Our example app simply retrieves a Poll resource by ID and prints out the question, along with the winning choice:

import sys
from typing import Dict

import requests


def get_poll(id: str) -> Dict:
    response = requests.get(f"http://api/poll/{id}")
    return response.json()

def get_top_choice(poll: Dict) -> Dict:
    return sorted(
        poll["choices"],
        key=lambda choice: -choice.get("votes", 0),
    )[0]

def display_poll(id: str):
    poll: Dict = get_poll(id)
    top_choice: Dict = get_top_choice(poll)
    print(f"""Question: {poll["question"]}
Winner: {top_choice["choice_text"]}
"""
    )

if __name__ == "__main__":
    display_poll(sys.argv[1])

Generating models

On the command-line, run

$ statham --input schemas/poll.json --output app/poll.py

Then open up the app/poll.py. You should see something like this:

from typing import List

from statham.schema.elements import Array, Integer, Object, String
from statham.schema.property import Property


class Choice(Object):
    choice_text: str = Property(String(maxLength=200), required=True)
    votes: int = Property(Integer(default=0))


class Poll(Object):
    question: str = Property(String(), required=True)
    choices: List[Choice] = Property(Array(Choice), required=True)

You can now import these generated models into your code to use as your representation of data described by the schema.

Using the models

With the models added, our app now looks like this:

import requests

from app.poll import Poll, Choice

def get_poll(id: str) -> Poll:
    response = requests.get(f"http://api/poll/{id}")
    return Poll(response.json())

def get_top_choice(poll: Poll) -> Choice:
    return sorted(poll.choices, key=lambda choice: -choice.votes)[0]

def display_poll(id: str):
    poll: Poll = get_poll(id)
    top_choice: Choice = get_top_choice(poll)
    print(f"""Question: {poll.question}
Winner: {top_choice.choice_text}
"""
    )

if __name__ == "__main__":
    display_poll(sys.argv[1])

This looks similar, but we get the following improvements:

  1. We will raise early with a specific validation error if we get bad data from the external source.

  2. We no longer need to handle the default value for votes.

  3. We can now use mypy to check how we are using the data - if our original code accidentally had poll.get("voets", 0), it would fail silently. Now mypy will tell us if we try to access a bad attribute.

Extending the models

Now that we have models for the external data, we realise that some of our logic belongs there! The models can be easily extended with properties and methods.

from typing import List

import requests

from statham.schema.elements import Array, Integer, Object, String
from statham.schema.property import Property


class Choice(Object):
    choice_text: str = Property(String(maxLength=200), required=True)
    votes: int = Property(Integer(default=0))


class Poll(Object):
    question: str = Property(String(maxLength=200), required=True)
    choices: List[Choice] = Property(Array(Choice), required=True)

    @classmethod
    def get(cls, id: str) -> "Poll":
        return cls(requests.get(f"http://api/poll/{id}").json())

    @property
    def top_choice(self) -> Choice:
        return sorted(self.choices, key=lambda choice: -choice.votes)[0]

    def __str__(self):
        return f"""Question: {self.question}
Winner: {self.top_choice.choice_text}
"""

Now our app logic becomes as simple as this:

import sys

from app.poll import Poll


if __name__ == "__main__":
    print(str(Poll.get(sys.argv[1])))

Note

When working with external schemas, it may be beneficial to preserve the generated models and extend them in sub-classes. This will help if you ever need to regenerate your models due to upstream changes.

This concludes the quickstart tutorial, please see the rest of the documentation for more detailed information.