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.)
Source code: https://github.com/PretzelLogix/py-cat-api/tree/05_result_model
2 replies on “Step 5: Creating a new Response model”
[…] This leads us to further enhancing our rest adapter with a custom Response model and Logging in the next chapter… Step 5: Creating a new Response Model […]
[…] Step 5: Create a new Response/Result model […]