User Owned Resources in FastAPI

undraw svg category

Welcome to Part 10 of Up and Running with FastAPI. If you missed part 9, 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

In the previous post we built out user profiles and ownership, and made sure our API returned user profiles when necessary. Now we’ll be getting to the meat of this application’s functionality. In this post, we’ll make users own cleaning resources they create and ensure that only the creators can manage their own cleanings jobs.

User-Owned Cleaning Jobs

Our database setup for the cleanings resource is rather naive. At the moment, we have no way of tracking who created a cleaning job. We’re going to fix that. And in doing so, we’re going to make it easy to create marketplace-style functionality.

Again, we’ll begin with the migrations file.

Migrations

Just like before, let’s start by rolling back our migrations.

Now go ahead and open up the file that looks like: db/migrations/versions/12345678654_create_main_tables.py.

We’ve moved the creation of the cleanings table to after we’ve created the users and profiles tables. The reason being is that we want to reference the users table when we define an owner column on the cleanings table. Here we set the value of that column equal to the id of the user that created it. This has multiple repercussions, but we’ll get to those in a minute.

Let’s go ahead and migrate the database by entering our docker container like before and running the upgrade head command.

Now that we’ve given users ownership of a cleaning resource in our database, we’ll want to refactor our code a bit. Any user should be able to access a cleaning resource, but only the user that created them should be able to update it and delete it. Users that want to list all cleanings should only recieve the ones that they themselves have created.

Usually, we’d start with testing. But since we’ve modified the database, let’s dip into the models/cleaning.py file first.

Modeling User Ownership

Looking at our file, we see:

models/cleaning.py

We’re adding the owner attribute to our CleaningInDB model that will an integer representing the id of the owning user. On top of that, we’re finally taking advantage of timestamps by utilizing DateTimeModelMixin in our CleaningInDB model. Our CleaningPublic model simply inherits everything from CleaningInDB, but specifies that the owner attribute can be either an int id of the user, or the actual UserPublic model itself.

If we were to run our tests at this point, most of them would error out. Feel free to try it out.

Let’s go ahead and fix that. We’re going to make quite a few updates, so don’t be concerned when our tests start breaking.

Starting with our test_cleaning fixture in the conftest.py file, modify it with the following code:

tests/conftest.py

We’re taking in the test_user fixture and sending it to the CleaningsRepository whenever a new cleaning is created. We’ll want to take that user and pass their id to the database as the owner attribute.

Let’s do that now.

Create Cleanings

Make the following changes to the CleaningsRepository.

db/repositories/cleanings.py

Though we’ve only updated the CREATE_CLEANING_QUERY, we’re going to need to change most of the SQL. We’ve added in the owner attribute, and we’re returning the timestamps as well. If we were to run our tests now, we’d still error out as we’re not passing the currently logged in user to the CleaningRepository. We’ll get to that in a moment.

First, let’s update our tests/test_cleanings.py file. We’re going to basically start from scratch, keeping what we like and discarding what we don’t need anymore.

tests/test_cleanings.py

We’ve added a new test to ensure that unauthenticated users can’t create cleaning opportunities, and we’re checking that any newly created cleanings have the currently logged in user as their owner.

Open up the api/routes/cleanings.py file and make that happen. We’ll start from scratch here too, going route by route.

api/routes/cleanings.py

We’re now using the same auth dependencies we defined in one of our previous posts, and we’re passing the logged in user to the CleaningsRepository. This way any new cleaning will have the currently authenticated user attached as the owner of the resource.

Run the test again and see that we’re a step closer. All the TestCreateCleaning tests are passing.

Get Cleanings

Now we’re moving on to the GET requests.

Let’s first add the next set of cleaning tests.

tests/test_cleanings.py

We’re doing a lot of the same thing here with fetching cleaning jobs. Users should only be able to get a cleaning resource if they’re authenticated, and when users ask to list all cleaning jobs, we only send back those that belong to them. Otherwise things are pretty much the same.

Let’s get these passing.

Open up the CleaningsRepository and update it with the following:

db/repositories/cleanings.py

We’re now expecting the requesting_user in each of our methods. Even though the get_cleaning_by_id method doesn’t use that parameter, it’s there for consistency. Besides, we’ll end up using it later anyway - so keep it there.

Let’s modify our routes to support these changes as well.

api/routes/cleanings.py

In both of these routes, we’re using the get_current_active_user dependency to protect the route. On top of that, we pass the user to our CleaningsRepository for all relevant database activity.

Only two more to go.

Update Cleanings

The tests for updating cleanings need only a few modifications.

tests/test_cleanings.py

All we’ve really done here is ensure that our authenticated user doesn’t have permission to update another user’s cleaning resource. We also make sure that a user can’t change the owner of their own cleanig resource. Everything else is the same.

And on to the CleaningsRepository.

db/repositories/cleanings.py

Wow! Our update_cleaning method has really ballooned in size! That’s ok, since we’re mostly handling edge cases. If we find ourselves doing this frequently, it might make more sense to build out the BaseRepository a bit more to handle a lot of the boilerplate. We won’t do that now since we’re so close to finished, but keep it in mind.

And for the route:

api/routes/cleanings.py

Much of the same old stuff here. The biggest difference is that we’re protecting the route with our get_current_active_user dependency and passing it to our repo. We won’t belabor the point anymore.

Run the tests again. So close.

One more to go

Delete Cleanings

Let’s polish this off nice and clean by finishing our DELETE route.

Add one last test class:

tests/test_cleanings.py

Our last update to the test_cleanings file checks to make sure that users can delete their own cleaning jobs and that they can’t delete other users’ cleaning jobs.

Let’s make that happen in our cleaning repo.

db/repositories/cleanings.py

We’re doing the same thing with our delete_cleaning_by_id methods that we did with all the others. Pass in the requesting_user and ensure that they’re only allowed to delete the cleaning jobs they own. Otherwise we raise the proper exception.

Last, but not least, the DELETE endpoint.

api/routes/cleanings.py

We run our tests and…finally! All passing.

But there’s something fishy about this. We’re duplicating code all over the place. We keep checking to see if a cleaning exists and raising a 404 exception if it doesn’t. We’re also raising 403 exceptions when the user isn’t allowed to modify or delete a resource.

Is there a better way to do that?

Well, actually there is. We can use FastAPI’s built-in dependency system to handle that for us. Let’s first create a new dependencies file for cleanings, and then we’ll refactor our code a bit.

And add a new dependency callable:

api/dependencies/cleanings.py

We’ve abstracted both of our common exceptions into dependencies that will help us manage access to any cleaning resource. Now we can simplify our routes and repository significantly. Let’s see how our updated CleaningsRepository looks first.

db/repositories/cleanings.py

There is significantly less code here now. The repository is now accepting a CleaningInDB model instead of an id for both the update and delete actions. Our dependency uses the get_cleaning_by_id method to handle all 404 issues, making our life much easier. We’ve also removed any references to requesting_user for our modification actions because our check_cleaning_modification_permissions dependency is handling that for us.

So how do we use them in our routes? Well, here’s where things get interesting.

We’ll look at new api/routes/cleanings.py file here as well.

api/routes/cleanings.py

Well, look at that. For each of our route functions, the body is a simple one-liner. Most of the work is being done by our dependencies. In fact, we’ve even added dependencies in the route decorator for our update and delete endpoints. The FastAPI docs provide more details on that pattern.

Our check_cleaning_modification_permissions dependency ensures that the user has sufficient permission to update or delete a cleaning, so we don’t have to do that ourselves.

Run the tests now and see that they’re all still passing.

It’s comforting to have all these tests in place when we refactor. Now we can be confident that our code is working as we expect it to even when we make large refactors such as this one. And this is a big improvement. We have much less code duplication and we’ve extracted permissions into its own system.

And there we have it! Refactoring is sometimes the least fun part of TDD, but it’s essential. We’ll do it a few more times before this series is over.

Wrapping Up and Resources

We’ve now set ourselves up to get into the meat of our application’s functionality. In the next post, we’ll give users the ability to offer their services for a cleaning job and let owners accept or reject a given offer.

However, now it’s time for a break.

Github Repo

All code up to this point can be found here:

Tags:

Previous Post undraw svg category

Setting Up User Profiles in FastAPI

Next Post undraw svg category

Marketplace Functionality in FastAPI