Legacy APIs and How To Integrate Them with Django, Celery, and A Lil' Bit of Tenacity

Mateusz Pydych
12 August 2021 · 8 min read

Good old medical services

How do you envision premedical IT services?

They should be reliable. After all, they support the medical sector, right? And they are reliable. Since I've started working for Medishout, I've never seen anything that fails spectacularly within the project. So what causes the problem? APIs do. The thing is peri-medical IT services are just... strange.

In Medishout, we have a few 3rd party integrations. Most of them were painful to do. The reason is that the services that we had to integrate with were just monolithic (or just old).

There were plenty of issues. Most of them only degraded the mood of the programmer but one, which is APIs outages, could affect the users as well. And if you work in peri-medical IT industries, you don't want to burden the users with your errors. This kind of error can cost someone their health, or even life.

The first thing that can be thought of is "If someone's API fails, he should just fix it" right? In most cases, they will, but it can take months onto years. Startups do not have that kind of time to wait.

In this article, I'll describe a case study of one of the integrations. You will see how to integrate external API in an overly complicated way, and why should we be bothered to try so hard.

Problem overview

At some point in Medishout evolution, we noticed that in some cases our users had to manually pass service requests to an external platform. Those requests are for administrators, to let them know that something is broken, and they have to repair it.

To make their lives easier, we decided to integrate with this external API, in such a way, that once a thing is requested to be fixed, we save it on our dashboard and after that, we pass it to a 3rd party.

During the development process, we noticed a few problems.

  • we had to perform more than one API request to pass the request as a whole
  • 3rd party responses were slow enough to aggravate the user
  • documentation and the overall look at the 3rd party convinced us, that making a few API requests is insufficient to make sure that the request is delivered successfully

Celery: healthy, tasty, and useful

Most likely, the audience of this article is familiar with Celery, an amazing library to perform long-lasting tasks in the background. If not, I recommend this amazing article written by Vitor Freitas (the whole blog is brilliant tho).

Now you might be thinking: "Hey Matt, why do you bring Celery in for such a simple task?".

Well, there are a few reasons:

  • Sometimes creating an entity in a third-party service requires more than one simple POST request, as someone might assume. That means, for example, that after I create an entity in a 3rd party database through API, I have to modify it with PUT request(s).
  • Our requests may fail. Therefore if we don't move the whole process to the background, we have insufficient time to retry or perform complicated logic on it (recall that on the other side of the screen, there is a user that waits for the server's response).

Let's get our hands dirty

With all these things in our minds, we can start to develop a solution. After we've created a separate Django app for this piece of the project, we can create ExternalAPIService in which we include all of the stuff needed to perform valid API requests to 3d parties.

#service.py

from django.conf import settings

import requests 

class ExternalAPIService:
    url = f"{settings.EXTERNAL_API_URL}/create/"
    headers = {
        "Authorization": f"Basic {settings.EXTERNAL_BASE_AUTH}",
        "Content-Type": "application/json",
    }
@classmethod
def create_request(cls, body):
    try:
        resp = requests.post(cls.url, headers=cls.headers, json=body)
    except requests.exceptions.HTTPError:
        raise Exception("Entity not created!")
    return resp

@classmethod
def update_request(cls, body):
    try:
        resp = requests.put(cls.url, headers=cls.headers, json=body)
    except requests.exceptions.HTTPError:
        raise Exception("Entity not updated!")
    return resp

By moving request related logic into a separate class we achieve a couple of benefits:

  • Response of API is easy to mock in tests
  • Usage in code is as simple as ExternalAPIService.create_request(body)
  • Logic related to 3rd party API is in one place, so further modifications should be fairly straightforward

The only thing that an attentive reader might consider unhandy, is this loose body parameter. In my case, I just encapsulated the logic related to creating a proper body in ExternalRequestBodyHelper, which works just fine. As it is not directly related to this article, I'll not discuss it here.

Using service in practice

After we’ve acquired well-separated logic responsible for API requests, we can create tasks that will use it.

# tasks.py

from our_project.celery import app
from external_api_integration.service import ExternalAPIService


@app.task(name="create_entity_request")
def create_entity_request(body, update_args):
    try;
        create_request_response = ExternalAPIService.create_request(body=body)
    except Exception:
        # After raising proper exception execute some logic, here is just a mock :)
        return
    update_entity_request.delay(create_request_response.json(), update_args)


@app.task(name="update_entity_request")
def update_entity_request(body, update_args):
    body.update(update_args)
    try:
        ExternalAPIService.update_request(body=body)
    except Exception:
        # After raising proper exception execute some logic, here is just a mock :)
        return

What happens here? We created two tasks: one uses ExternalAPIService to create requests, the other uses update_args to update created entity with target data. Note that possible failure should be caught and handled with try-except structure, but to keep it simple that part of the code was replaced with an appropriate comment.

Alright, after we execute these tasks in some convenient place (perhaps in one of APIView methods), we move the process to the background, thanks to Celery. The user will not have to wait for a response. He only receives confirmation that everything will be processed. So everything is just fine. Except it's not.

Handling APIs outage using Tenacity

In the whole process, we want to add some robustness to communication with 3rd party. Moving the process to the background tasks ensures us that a user will not have to wait for a response all day, but it won't save us from things that might go wrong during creating and updating entity processes.

One reasonable thing we can do is to keep trying.

This is where Tenacity shines. It's a terrific library for python, that allows you to repeat tasks in a controlled manner. So if we modify ExternalAPIService a little, we can end up with methods that will try to communicate with external API multiple times before they give up. See the example below, of how simple it is with the Tenacity package.

# service.py

from django.conf import settings

import requests 

class ExternalAPIService(object):
    url = f"{settings.EXTERNAL_API_URL}/create/"
    headers = {
        "Authorization": f"Basic {settings.EXTERNAL_BASE_AUTH}",
        "Content-Type": "application/json",
    }


@classmethod
def create_request(cls, body):
    try:
        resp = requests.post(cls.url, headers=cls.headers, json=body)
    except requests.exceptions.HTTPError:
        raise Exception("Entity not created!")
    return resp

@classmethod
def update_request(cls, body):
    try:
        resp = requests.put(cls.url, headers=cls.headers, json=body)
    except requests.exceptions.HTTPError:
        raise Exception("Entity not updated!")
    return resp

@classmethod
@retry(
    wait=wait_exponential(multiplier=1, min=4, max=10), reraise=True, stop=stop_after_attempt(10)
)
def tenacious_create_request(cls, body):
    try:
        return cls.create_request(body)
    except Exception:
        return None # I'm just a silly example. Handle me with some logic if I fail.

@classmethod
@retry(
    wait=wait_exponential(multiplier=1, min=4, max=5), reraise=True, stop=stop_after_attempt(10)
)
def tenacious_update_request(cls, body):
    try:
        return cls.update_request(body)
    except Exception:
        return None # I'm just a silly example. Handle me with some logic if I fail.

Two new methods were added to ExternalAPIService. Both with the same idea: repeat the request a few times, before failing.

Tenacity allows repeating things in many ways. In this case, in my opinion, the best way to use it is wait_exponential functionality. Using this method will result in each subsequent repetition being performed with a greater interval of time that grows exponentially with a * 2 ^ x. It seems to be completely reasonable to try a few times first, and wait a little longer if the attempts still fail.

As much as we try to deliver a request, at some point we should just give up. If something is just broken, an error should be thrown to indicate to the user that his request will not be delivered, and he should sort things out some manual way. The best thing that can be done in such a case is a combination of reraise and stop_after_attempt methods of the Tenacity library. The first one will ensure that an exception, which was thrown inside the retry loop, will be propagated (instead of default RetryError). The second one is pretty straightforward, it allows us to specify how many attempts we want to do.

Wrapping it up

The last thing to do is to use tenactious_requests in Celery tasks. Nothing easier:

# tasks.py

from our_project.celery import app
from external_api_integration.service import ExternalAPIService

@app.task(name="create_entity_request")
def create_entity_request(body, update_args):
    create_request_response = ExternalAPIService.tenacious_create_request(body=body)
    if not service_request_response:
       raise Exception("Could not create entity for external partner.")
    update_entity_request.delay(create_request_response.json(), update_args)

@app.task(name="update_entity_request")
def update_entity_request(body, update_args):
    body.update(update_args)
    update_request_response = ExternalAPIService.tenacious_update_request(body=body)
    if not update_request_response:
       raise Exception("Could not update entity for external partner.")

And that's it. We ended up with background tasks that will do their best to pass user requests to a 3rd party. Using a service pattern makes it easy to test, and try-except structure provides the developer with a convenient way to handle cases when something goes wrong.

Final notes tl;dr.

In this article we have seen how to create a piece of software that will handle complicated communication processes with a third party. By using Celery, we moved the whole process to the background, so a regular user does not have to wait until the process finishes. With Tenacity, we added robustness to communication with third-party API.

In the shown flow, we do not track the state of a 3rd party request (whether it's updated or just created). I decided not to put it in this article, but if you, dear reader, consider implementing such a thing in your system, check Django Finite State Machine. It will make your life a lot easier.

Join our team! We are always open to meet savvy people!

See open positions at Ulam

Read also:

What is Django used for?

Flask vs Django

Flutter vs React Native vs Ionic

Share on
Related posts
DevSecOps Explained: Important Questions and Answers
PYTHON

DevSecOps Explained: Important Questions and Answers

To put it simply - it’s another approach to software making. It has been derived from DevOps, where developers (Dev) and operational engineers (Ops) combine their skills from the start of the project…
4 min read
Flask vs Django - Why NOT Choosing Flask Is Your Best Bet In 2020?
PYTHON

Flask vs Django - Why NOT Choosing Flask Is Your Best Bet In 2020?

Outline What is Flask? And what is Django? How to compare these two frameworks? What is Flask used for? What is Django used for? Frameworks trade-offs Flask and Django comparison - a contrarian…
11 min read

Tell us about your project

Get in touch and let’s build your project!