Categories
Code

Step 5: Creating a new Response model

In our last chapter, we talked about properly catching exceptions and re-raising with a custom exception in our _do method:

def _do(self, http_method: str, endpoint: str, ep_params: Dict = None, data: Dict = None):
    full_url = self.url + endpoint
    headers = {'x-api-key': self._api_key}
    try:
        response = requests.request(method=http_method, url=full_url, verify=self._ssl_verify, 
                                    headers=headers, params=ep_params, json=data)
    except requests.exceptions.RequestException as e:
        raise TheCatApiException("Request failed") from e
    data_out = response.json()
    if response.status_code >= 200 and response.status_code <= 299:     # 200 to 299 is OK
        return data_out
    raise Exception(data_out["message"])

In this chapter, we’re going to refactor our code again to make what _do returns more generic and more useful to any code that consumes it.

If you’ll recall the requests library that we’re using to make our HTTP calls has a Response object. After every call with the request method, we get a Response back. This is a complex object with lots of information in it. This REST API adapter that we’re writing is all about simplifying things. So let’s also make a simple Result class of our own to pass only the most essential information on.

In the Project file tree explorer in the_cat_api module, add a new Python file and call it: models.py

from typing import List, Dict

class Result:
    def __init__(self, status_code: int, message: str = '', data: List[Dict] = None):
        self.status_code = int(status_code)
        self.message = str(message)
        self.data = data if data else []

This class is a simple data model that is designed to only carry the essential data of the HTTP transaction:

  • the status_code (int)
  • a message (if any)
  • a list of dictionaries (if any).

Let’s integrate this new class with the rest adapter.


Going back to rest_adapter.py, add the following lines in the imports section:

from json import JSONDecodeError
from the_cat_api.models import Result

In the last step, we started using the customer exception: TheCatApiException. Now let’s properly handle any possible exceptions for the response.json() parsing line.

try:
    data_out = response.json()
except (ValueError, JSONDecodeError) as e:
    raise TheCatApiException("Bad JSON in response") from e

The .json() method can raise different exceptions. The ones most commonly experienced are ValueError and JSONDecodeError. In this try-except block, we’re catching both types. And then we’re using our new exception again: TheCatApiException


We might as well finish the rest of the _do() method. Look at line 29 and the squiggly line under it. If we click somewhere in the line and put our mouse over the line, the PyCharm IDE suggests refactoring the code.

Click on the Yellow light bulb (💡) and click Simplify chained comparison and it automatically refactors that line for you.

We finish refactoring this last part of _do() by returning a Result object instead. From response, we include status_code, reason, and data_out that we got from parsing the JSON.

If the status_code isn’t in the 200 range, then we raise TheCatApiException with status_code and reason.

    if 299 >= response.status_code >= 200:
        return Result(response.status_code, message=response.reason, data=data_out)
    raise TheCatApiException(f"{response.status_code}: {response.reason}")

We’ve done something new here. We’re taking advantage of a Pythonic syntax that allows us to more simply test something in a range.

Side note: Why are we testing 299 >= x before x >= 200? The common failure case is either a 400 or 500 error. It’s exceedingly rare for the status code to be under 199. This way when an error does happen, we detect it more quickly. Admittedly, it’s only a few clock ticks faster, but it’s still faster and it’s good practice.

So this new code is much better, right? The first line tests if the status code is in the 200-range or not and returns the Result. Otherwise, it raises an exception.


That’s DRY code.


Let’s go up to the def _do() line and type-hint that we’re returning a Result now.

def _do(self, http_method: str, endpoint: str, ep_params: Dict = None, data: Dict = None) -> Result:

Let’s do the same for the other methods:

def get(self, endpoint: str, ep_params: Dict = None) -> Result:
    return self.do(http_method='GET', endpoint=endpoint, ep_params=ep_params)

def post(self, endpoint: str, ep_params: Dict = None, data: Dict = None) -> Result:
    return self.do(http_method='POST', endpoint=endpoint, ep_params=ep_params, data=data)

def delete(self, endpoint: str, ep_params: Dict = None, data: Dict = None) -> Result:
    return self.do(http_method='DELETE', endpoint=endpoint, ep_params=ep_params, data=data)

Not bad! Our REST adapter is starting to shape up and we’re only up to 40 lines! (Not including the lines in the models.py and exceptions.py file, of course.)

Step 6: Add Logging


Source code: https://github.com/PretzelLogix/py-cat-api/tree/05_result_model

2 replies on “Step 5: Creating a new Response model”

Leave a Reply

Your email address will not be published. Required fields are marked *