Populating Cleaning Jobs with User Offers in FastAPI

undraw svg category

Welcome to Part 25 of Up and Running with FastAPI. If you missed part 24, you can find it here .

This series is focused on building a full-stack application with the FastAPI framework. The app allows users to post requests to have their residence cleaned, and other users can select a cleaning project for a given hourly rate.

Up And Running With FastAPI

Our last post saw us finish transitioning our UI logic into custom hooks. With much of the tedious labor out of the way, our React app is nearing it’s final form. That’s right. We’re in the endgame now.

But we shouldn’t count our chickens before they hatch. There’s still work to be done. In particular, cleaning jobs being sent from our FastAPI backend are missing offers. Along with that, we’ve also removed most of the offer-related UI logic. We’ll need to patch both of those issues. The UI stuff we’ll put off for another day.

On the API side of things, we could return to our old method of fetching offers for a cleaning job when a given component is mounted. However, it probably makes more sense to populate cleaning jobs with their associated offers when they’re sent to the frontend. So for the time being, we’ll leave the world of React hooks behind and head to FastAPI land for some backend updates.

Updating the CleaningPublic Model

We’ve populated models with associated data before, so let’s start by naively assuming that previously established pattern will work as is.

Since we’re adding offers to a cleaning job, go ahead and import the OfferPublic model into the models/cleaning.py file.

models/cleaning.py

Uh oh. As soon as we save the file, we see an error message printed to the terminal:

This is the well-documented circular import problem with Pydantic models (for the uninitiated, this is actually a python thing and not simply a hurdle specific to Pydantic). See discussions about this issue here , here , and here .

The reason we’re seeing the error is that we’re importing the CleaningPublic model in our models/offer.py file and importing the OfferPublic model into the models/cleaning.py file. A dependency graph generated for our project would now have a cycle that looks like this:

circular imports in python
Circular imports in python

There are a few valid ways to address this issue. Let’s see a few of them in action and then make a decision about which one makes the most sense.

For the first approach, we’ll use the Pydantic docs on self-referencing models to guide our thinking. The page sheds some insight on our predicament and provides us with postponed-annotations as the core solution.

Let’s see how that would look:

models/cleaning.py

That’s pretty yucky, but it works. Beyond the fact that moving the import statement to the bottom of the file violates PEP-8, we’re also using we’re refering to the not-yet-constructed OfferPublic model using a string. Then, we import the OfferPublic model itself and call update_forward_refs so that Pydantic knows to prepare any ForwardRef types existing in the model. The string syntax seen here - List["OfferPublic"] - signals to Pydantic to internally resolve the field (string or ForwardRef) into a type object.

Now, with python 3.7, we can get rid of that ugly string syntax as long as we import annotations from the __future__.

models/cleaning.py

Better, but still not great.

Another solution would involve importing the non-public models from models/cleaning.py and models/offer.py into a single file (like models/public.py or something) and then handling interrelated references there. That will solve the circular import problem, but doesn’t seem very clean and would require updating imports in most of our files.

models/public.py

Not the biggest fan of this approach either, but it also works just fine.

The last proposal we’ll consider is simply removing the cleaning attribute containing our CleaningPublic model from the OfferPublic model. Does an offer really need to have the full cleaning object attached to it?

This approach would fix our circular import problem, but it forces us to change the way our application works. There’s definitely an argument to be made about removing circular dependencies whenever possible, though at the end of the day it’s really up to us to decide what best fits our use case.

For this series, we’re going to stick with our modified first approach using postponed-annotations:

models/cleaning.py

Unfortunately, that small change broke our tests! Run them and see what’s broken.

We’ll need to go in and modify a few things to support these extra attributes on our CleaningPublic model.

First, modify the repositories/cleanings.py file with a simple fix to the update_cleaning method:

repositories/cleanings.py

We’re excluding the two new attributes - offers and total_offers - from the dictionary we use to update a cleaning record, which should fix most of the broken tests. Run them again and watch them pass.

Great, now all that’s left is to fix the test_get_cleaning_by_id test as well.

test_cleanings.py

And all our tests should now be passing! With that exploration out of the way, it’s time to move on to actually populating cleaning jobs with the proper offers.

Populating Cleaning Jobs with their Offers

We’ll start with some new tests. Add this fixture to the tests/conftest.py file:

test_cleanings.py

Next, let’s add two new test cases in our tests/test_cleanings.py file that use this new fixture:

test_cleanings.py

Here we’re first testing that when a user requests their own cleanings, each cleaning comes back populated with all of its associated offers. We check that the numbers of offers is correct, and that they are associated with the proper user and cleaning job.

The second test case looks at requesting info about a single cleaning job from a user who is not the owner of the cleaning job, nor one of the users making an offer for that job. We ensure that the cleaning job comes back with information about the total number of offers made for the job, but none of the actual offers themselves.

Run those tests and watch them fail.

To get them passing, we can now follow the same strategy we’ve used before when populating models.

Start by adding the OffersRepository to the CleaningsRepository:

repositories/cleanings.py

Then, add a populate parameter to the list_all_user_cleanings method and populate each cleaning if that parameter is True.

repositories/cleanings.py

Now you’ll notice here that we’re calling the populate_cleaning method with a new parameter called populate_offers. When True, this should signify that we want to add the actual offer objects themselves to the cleaning.

Let’s update the populate_cleaning method now to support our new feature.

repositories/cleanings.py

We’re now fetching offers for a cleaning job whenever we want to populate one. Doing so makes it easy to set the total_offers attribute on our cleaning job, and include the full offers list itself if populate_offers is True. If populate_offers is not specified, then we only include the offer made by the authenticated user - if one exists.

This is mildly inefficient, since we only need to know the number of offers when populate_offers is False. At the moment, it’s not too concerning. When the time comes, we’ll add that to the list of things we want to optimize.

Run those tests again and the should all be passing! Except one.

We’ll also need to update our existing test case for users requesting their own cleanings now that we’re populating them by default and returning full CleaningPublic models populate with their owners.

Make the following updates:

test_cleanings.py

Ahh much better. All green tests now!

We’re almost there. There are still a few changes we should make on the backend to ensure that offers owned by the authenticated user are being populated as well. Starting with the OffersRepository, make the following updates.

repositories/offers.py

This might look like a lot of changes, but all we’re really doing is returning OfferPublic instance in places where we originally had an OfferInDB instance and then adding a full user instance to it. In places where we already have access to authenticated user we simply add them to the OfferPublic instance. Otherwise, we call the populate_offer method to handle that for us.

We’ll need to make one small change to the routes/offers.py file as well, as at the moment we aren’t passing the anything to the requesting_user parameter in our create_offer_for_cleaning method.

routes/offers.py

Nice! Now we have populated offers pretty much everywhere we expect them to be.

As usual, with modifications to our repositories come broken tests. And of course, running our tests show exactly that. Make the following changes to the tests/conftest.py file to fix things most of them temporarily.

conftest.py

Ah. That wasn’t so bad. All we did was make sure that every call to offers_repo.create_offer_for_cleaning also included the user making the request.

And those tests should now be passing again.

Let’s finish up this post by also populating any cleaning jobs owned by the authenticated user.

Open up the repositories/cleanings.py file and update the CleaningsRepository like so:

cleanings.py

Not too much to see here either. We’re returning a CleaningPublic instance from our create_cleaning method and explicitly setting total_offers to 0, as it’s not possible for a new cleaning job to already have offers. And in the update_cleaning method, we call our populate_cleaning method to attach offers to it. That should be totally fine, as we know the authenticated user is the owner of this cleaning job.

Just like before, our tests are breaking again. All that’s left is to make some simple fixes to tests/test_cleanings.py and we can be on merry way:

cleanings.py

Three changes in total:

And if we run our tests again, they should all be passing.

Now we can head back to the UI and see how things are looking. But not today.

Wrapping Up and Resources

In this post we finished populating cleaning resources with offers that other users have made for those jobs. To do so, we had to explore the classic circular imports problem and make substantial updates to our OffersRepository and CleaningRepository classes. Fortunately, we had a robust testing suite that informed us when things were breaking and we made the necessary updates to remedy each of our issues.

With that out the way, the next step will be to update our frontend to take advantage of our new structure.

Tags:

Previous Post undraw svg category

Refactoring Our UI Into Hooks - Part II