Making REST API calls in python is quite common and easy especially due to existince of libraries such as requests.
Take the following example:
def get_user(user_id):
res = requests.get(f'https://jsonplaceholder.typicode.com/users/{user_id}')
if res.ok:
return res.json()
else:
print(f'{res.status_code}: {res.text}')
return {}
The code is pretty straightforward. We are defining a function to fetch a user's details from an API (A very handy web app for testing out web apps).
Simple. Right?
However, what happens when there is no connectivity? Exceptions are raised.
This can easily be wrapped in a try...except
clause but it makes our code messy, especially when we have multiple endpoints to call.
Enter returns.
Returns is a library written with safety in mind. It's quite powerful and has a lot to offer. Now let's quickly fix our unsafe code.
class GetUser(object):
@pipeline
def __call__(self, user_id: int) -> Result[dict, Exception]:
res = self._make_request(user_id).unwrap()
return self._parse_json(res)
@safe
def _make_request(self, user_id: int) -> requests.Response:
res = requests.get(f'https://jsonplaceholder.typicode.com/users/{user_id}')
res.raise_for_status()
return res
@safe
def _parse_json(self, res: requests.Response) -> dict:
user = res.json()
return user
get_user = GetUser()
We have defined a class which can be invoked as a function once instantiated. We need to take note of the @safe
decorator, a very handy decorator which wraps any exception raised into a Failure
object.
The Failure
class is a subclass of the Result
type which is basically a simple container.
Now, our code changes from:
try:
user = get_user(1)
except Exception:
print('An error occurred')
to:
user = get_user(1) # returns Success(dict) or Failure(Exception)
user = user.value_or({})
Isn't that just elegant?
I know what you are thinking. It's too much code!
That's the same thing I thought at first. But after playing around with the library and a bit of customization, I started to notice that my code looked cleaner. I could quickly scan through a file and get the gist of what is going without have to follow references. This is just the surface. Returns has way more to offer. Check it out.
Here's a decorator I came up with to automatically unwrap Result
inner values or provide a default value instead:
def unwrap(success=None, failure=None):
def outer_wrapper(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
if is_successful(result):
if success is not None:
return success
else:
if failure is not None:
return failure
return result._inner_value
return wrapper
return outer_wrapper
Usage:
@unwrap(failure={}) # If a failure occurs an empty dict is returnd
@pipeline
def __call__(self, user_id: int) -> Result[dict, Exception]:
...
user = get_user(1) # returns dict or empty dict
Cheers!
Top comments (0)