Skip to content

Here I'm practicing using FastAPI while taking online course (https://www.youtube.com/playlist?list=PLeLN0qH0-mCVQKZ8-W1LhxDcVlWtTALCS). BTW i'm trying to improve the code from the course. Thanks to Artem Shumeiko for this course!

Notifications You must be signed in to change notification settings

YuriiMotov/FastAPI_study

Repository files navigation

FastAPI_study

Here I'm practicing using FastAPI while taking FastAPI online course. Along the way I'm trying to improve the code from the course.

Thanks to Artem Shumeiko for this course!

Table of contents:

  1. Lesson 5 (user registration and authentification with fastapi-users)

  2. Lesson 6 (routers and project file structure)

  3. Lesson 7 (designing a RESTful API)

  4. Lesson 8 (caching with Redis)

  5. Lesson 9 (background tasks with Celery, Redis and Flower)

  6. Lesson 10 (testing with PyTest)

  7. Lesson 11 (Linking Frontend and Backend. Cors and Middleware)

  8. Lesson 12 (Jinja templates)

  9. Lesson 13 (Websockets)

  10. Lesson 14 (How to use Depends)

  11. Lesson 15 (Docker and Docker Compose))

  12. Lesson 16 (Software deployment on render.com)

  13. Layered architecture style

  14. UnitOfWork pattern with FastAPI

  15. Reading Fast API documentation and practicing new things

    15.1. OAuth scopes

    15.2. Sub Applications - Mounts

    15.3. Testing WebSockets

    15.4. OAuth2 with refresh tokens + rotation

    15.5. Request rate limit

    15.6. SQLModel

    15.7. Monitoring FastAPI with Prometheus and Grafana

    15.8. Authentication with Keycloak

Lesson 5 (user registration and authentification with fastapi-users)

Watch original lesson on Youtube

  1. Why do we have to use 2 differend drivers for PostgreSQL (psycopg2 for fastapi, and asyncpg for alembic)? Let's use psycopg3 in both places!

    Commit: 3f22c0b

  2. Getting rid of duplicating User database model . In the original lesson's code there are two places where this model is declared (models.model.py and auth.database.py) and you have to maintain both of these models in the same condition. It's bad and I think we can declare it once in models.model.py and inherit from SQLAlchemyBaseUserTable. Also, new version of fastapi-users documentation follows the orm style to declare this table. So, let's use the same style in models.py.

    Commit: 5034ae2

  3. It's said in course that SECRETS for fastapi-users should be stored in .env. Move them to .env.

    Commit: fa9517c

  4. I don't like the idea of overriding the create method in UserManager class just to set default role_id value. We can easily do it by specifying the default parameter in User model and removing role_id from the UserCreate schema.

    Commit: 8a2269c

  5. In addition to 3 already implemented API endpoints (register, login, logout), let's implement forgot_password and update endpoints.

    Commits: 8890f19 and eee3d60

  6. Cookie transport is useful if you use web-browser. To use this API from mobile apps or from other systems, let's learn how to use Bearer transport and Database strategy.

    Commit: c9369cb

Lesson 6 (routers and project file structure)

Watch original lesson on Youtube

  1. Changing project structure and adding operations module as it's shown in lesson's source code.

    Commit: bc4f100

  2. Using one metadata object for all database models, using ORM-style to declare operations table. Getting rid of depricated dict method in operations.router. Renaming base_config to backends.

    Commits: 01e52f5, bc7c563

  3. Grouping auth routes into module router and include this router in main.

    Commit: 6fbd3fd

  4. Configuring the migration file name format to include date and time.

    Commit: bfb7e1c

Lesson 7 (designing a RESTful API)

Watch original lesson on Youtube

  1. Implementation of changes made in the lesson #7

    Commit: c8b1b78

  2. Doing lesson's homework #1 (using HTTP PUT and PATCH methods)

    Commit: 27fab33

  3. Doing lesson's homework #2 (standardization of input and output of all the endpoint interfaces).

    Speaking about standartization of endpoints, I don't think that suggested in this lesson approach is good. Fastapi-users doesn't follow this approach and we have to modify that endpoints to make them similar.

    I don't really understand why we need to add additional fields status and detail to successful response. At the same time we dont need some of these fields in other types of responses.

    We have HTTP response status code for passing status and I think it's better to use it instead of additional status field. The response to a successful request will contain only data (at the first level, without the additional data field), for unsuccessful requests it will contain the detail field (as it is done in fatsapi-users).

    In addition, let's specify the response schemes for the endpoints of the operations module.

    Commits: 1eedb71 and 520b166

  4. Doing lesson's homework #3 (pagination of results)

    Commit: fb0c767

  5. Catching database error, doesn't exist and already exist errors. Moving the handling of common errors outside of endpoint functions.

    Commit: a0ecbdd

  6. Let's make the endpoints more RESTful: apply the REST URI construction rules (plural name for collection, id in the URI for GET, PUT and PATCH methods). It changes the logic of POST and PUT methods. Now POST will not accept id (it will be autogenerated) and PUT will return error if record with requested id doesn't exist (before these changes the PUT method created new record if record with requested id doesn't exist).

    Commit: 3d2f69e

Lesson 8 (caching with Redis)

Watch original lesson on Youtube

  1. Implementation of changes made in the lesson #8

    Commit: 8b28197

  2. Let's try to use fastapi-cache with InMemoryBackend. Note that InMemoryBackend store cache data in memory and use lazy delete, which mean if you don't access it after cached, it will not delete automatically.

    Commit: d4ea3e3

  3. Let's learn how to manage client-side caching by setting headers.

    Commit: cb97d4f

  4. Delving into fastapi-cache library. Here are some problems (or potential problems) of this library that I found:

    3.1. Caching doesn't work for private endpoints. If I pass a user object to my endpoint function and try to use this endpoint by opening in browser, it isn't cached. It's cached only on client side. I'm sure this problem can be solved by implementing custom key-builder, but it looks like a feature that should be by default.

    3.2. For private methods this library generates cache-control: max-age=N headers, but it doesn't add Cache-Control: private. This can lead to the leakage of personal data if the proxy server caches this data.

    3.3. There is no parameter to disable client-side cache headers in cache decorator. And you can't just override it in you function by adding response.headers["Cache-Control"] = "no-store" (they are added after the function call and will be overrided). People write middleware to do this, which is not good.

    3.4. There are quite a lot of issues on project's github page and some PRs. It looks like project owner doesn't have enought time to continue developing this project..

Lesson 9 (background tasks with Celery, Redis and Flower)

Watch original lesson on Youtube

  1. Implementation of changes made in the lesson #9

    Commit: 2d0f98f

  2. Let's add a background task execution check. If there is a problem during tha task execution then system will call special function.

    1.1. With FastAPI BackgroundTasks we can add try..except block in the background task's function and call our ErrorCallback function if any exceptions occur.

    Commit: 8063d38

    1.2. With Celery we can handle task's execution errors different ways:

    1.2.1. On the worker's side by using signals or specifying base class for task.

    Commit: ff65eee

    1.2.2. On the FastAPI side by adding special async task for monitoring celery's task statuses. You can also implement it with Celery events real-time processing (you should run it in a separate thread), but I prefer first variant.

    Commit: c3e3271

  3. The use of FastAPI's on_startup and on_shoutdown events is deprecated, we should use lifespan instead.

    Commit: fd61e6b

  4. Adding celery-task execution monitoring endpoins.

    Commit: ce7f425

  5. Playing with celery-task priorities.

    4.1. Redis priorities. This approach can be used if you use Redis as a backend, your tasks are not long and you do not need very high prioritization. It's important to run worker with --prefetch-multiplier=1 option. Otherwise Celery will preload (CPU_cores_count)*4 tasks from the queue by one request.

    Commit: fdca370

    4.2. More common approach is to separate queues and run multiple workers for different queues.

    Commit: f0d87c3

Lesson 10 (testing with PyTest)

Watch original lesson on Youtube

  1. Implementation of changes made in the lesson #10

    Commit: ff80cfe

  2. Understanding the neccessety of the event_loop fixture.

    As I learned from the python-asyncio documentation, we need to override default event_loop fixture if we use fixtures with the scope different to function wich is the default scope. So, since we use fixtures with session scope, we need to override event_loop fixture with the same scope (it might be any scope that is equal to or wider than others).

  3. Testing endpoints, that use '@cache' decorator.

    As httpx.AsyncClient doesn't implement the lifespan protocol, it doesn't use (evoke) lifespan context where fastapi-cache is initialized. The solution is to add just one line of code in the ac fixture.

    Commit: 6b10367

  4. Is it possible and beneficial to run asynchronous tests in parallel?

    Yes. We can use pytest-asyncio-cooperative plugin to achieve it.

    It seems as if it doesn't make sence to run tests in cooperative mode if you have a lot of light (fast) tests. But if your tests require long I/O, it will definitely benefit.

    Commit: c5abf7b

Lesson 11 (Linking Frontend and Backend. Cors and Middleware)

Watch original lesson on Youtube

  1. Implementation of changes made in the lesson #11

    Commit: c7aa64d

  2. Checking how it works.

    Since I don't have the Frontend's code that was demonstrated in video, I used browser's console and fetch to check how it works.

    I set origins in main.py as: origins = ["https://fastapi.tiangolo.com"], opened https://fastapi.tiangolo.com in browser, then opened inspect -> console.

    Add operation:

     fetch(
         'http://localhost:8000/operations/',
         {
             method: 'POST',
             headers: { 'Content-Type': 'application/json' },
             body: JSON.stringify({"quantity": "10", "figi": "string", "instrument_type": "BTC", "date": "2023-10-26T08:29:31.139Z", "type": "sell"})
         }
     ).then(resp => resp.text()).then(console.log)
    

    Get operations:

     fetch(
     'http://localhost:8000/operations/?operation_type=sell',
     {
         method: 'GET'
     }
     ).then(resp => resp.text()).then(console.log)
    

    Authorization via Bearer:

     fetch(
     'http://127.0.0.1:8000/auth/bdb/login',
     {
         method: 'POST',
         credentials: "include",
         headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
         body: "username=EMAIL&password=PWD"
     }
     ).then(resp => resp.text()).then(console.log)
    
     fetch(
     'http://127.0.0.1:8000/auth/me',
     {
         method: 'GET',
         credentials: "include",
         headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer TOKEN_RECEIVED_IN_PREVIOUS_STEP' }
     }
     ).then(resp => resp.text()).then(console.log)
    

    I could not check authorization via Cookies.. I'll try to figure out later.

Lesson 12 (Jinja templates)

Watch original lesson on Youtube

  1. Implementation of changes made in the lesson #12

    Commit: 3e96cf7

  2. Thoughts about error handling.

    What happens if an exception occurs during request processing?

    This exception will be handled by FastAPI exception handlers that we set. And it will be the same handler for all requests (API calls all webpage requests).

    I want to separate these requests and show html-page for web requests and json for API requests.

    It turned out that you can't set different exception handlers for different routers. After doing some research I decided that the best way to implement that is to run two different ASGI-applications: first (API-server) will include routers for API requests, second (WEB-server) will include routers for WEB requests. And both of them will have their own exception handlers. You can run these servers separately (in two different terminal sessions) or write a script which will run two servers in one event loop.

    Commit: f104992

  3. Getting rid of src/ in paths.

    When I tried to integrate lesson's 12 code to my code, it didn't work until I added src/ to the beginning of the paths to static files and templates.

    It happened because I ran code from current project's directory, not from src directory. To run code properly you just need to change dirrectory in terminal before you run server.

     cd src
    

    Now it works without src/ in paths.

    Commit: a929c00

Lesson 13 (Websockets)

Watch original lesson on Youtube

  1. Implementation of changes made in the lesson #13

    Commit: c098aec

  2. Some code refinements:

    • Move ConnectionManager to separate file, rename it to ChatManager

    • I think the "ChatManager" should encapsulate all chat logic, let's move all database operations to its methods and pass session as a parameter.

    • Getting rid of underscores in URL (it's recomended to use dashes instead)

    Commit: f3201b9 and fix: 7d4ced6

  3. Error handling.

    Now websocket endpoint is running under WEB-server and if this endpoint fails server will answer with HTML-page. That's not correct, because javascript expets json answer.

    We could add TRY..EXCEPT blocks to the endpoints that return JSON. Or we can move these endpoints to the API-server. I've chosen second variant. And after that it's needed to add error handling in JavaScript code, but it's not my job :)

    Commit: 90e7015

  4. In FastAPI documentation it's recomended to use encode/broadcaster for more complex tasks. Let's implement the same functional with this library.

    It turned out that there is a problem: this library doesn't support message history. And it looks like nobody is going to add this support in the near future..

    But it can be useful if you are developing a multiservice application and you need all instances to have a common message queue.

    Commit: 71ed85b

  5. Alternatives of Websockets.

    nice article

Lesson 14 (How to use Depends)

Watch original lesson on Youtube

  1. Implementation of changes made in the lesson #14

    Commit: f4b6d11

  2. It's recommended in the FastAPI documentation to use Annotated instead of passing Depends as a default attribute value.

    Commit: 00734e4

  3. Let's make oauth2 example more secure by following the FastAPI documentation

    Commit: 5003291

  4. Figuring out the dependencies execution order.

    When somebody call the endpoint which has dependencies, FastAPI builds the tree of dependencies and call them in right order. Results are cached, so if you have several instances of one dependancy, it will be called once (see the operation-with-dependencies-1 endpoint).

    Dependancies with yield are executed till the yield.

    After that the endpoint function is executed.

    If any exception occures during this process (till the moment then Response is sent), this exception will be passed to the dependencies with yield. You can catch it and raise other exception, including HTTPException (although, it's better not to do this), which changes HTTP-Response.

    After the endpoint's function has executed successfully and Response has sent to the client, background task starts.

    [Depricated] If any exception occures during the background task execution, this exception will be passed to the dependencies with yield. You can catch it and do whatever you want except raising HTTPException (it doesn't make sence since the Respons has sent and it will couse another exception (RuntimeError: Caught handled exception, but response already started)).

    Commit: f805aea, fix: 8c7b46d

    Update: From version 0.106.0 using resources from dependencies with yield in background tasks is no longer supported. And now it's OK to raise HTTPException in dependencies after yield.

    Commit: 73a36eb

Lesson 15 (Docker and Docker Compose)

Watch original lesson on Youtube

  1. Implementation of changes made in the lesson #15

    Commit: 0b1edeb

  2. User registration via fastapi-users doesn't work for now, because there are no any roles in DB yet.

    Let's add script wich will insert initial data into DB.

    Commit: b1401a2

  3. At the moment, all the data is stored in the container and will be deleted if you delete the container. Let's add volume to make DB data persistent.

    Now DB data is stored separately from container in /var/lib/docker/volumes and won't be deleted if you delete container.

    Commit: 4ee7e29

  4. This version of docker-compose file runs only api-server. But we need to run web-server too.

    Commit: 595ac10

Lesson 16 (Software deployment on render.com)

Watch original lesson on Youtube

  1. Implementation of changes made in the lesson #16

    Commits: 79d4a9d and cfd6b0a

  2. It's not good that we had to override Dockerfile to deploy our app on render.com. By doing that our docker-compose solution was broken.

    Render.com allows to specify the directory where it will look for Dockerfile.

    Just copy Dockerfile to docker/render_com/ in the github-repository and change in the web-app settings on render.com Dockerfile Path from ./Dockerfile to ./docker/render_com/Dockerfile. Then trigger the deployment hook.

    Revert changes of Dockerfile placed in the root directory of the repository to make docker-compose solution work again.

    Commit: 662899b

  3. Let's also run our web-server.

    To do that just create one more app with the same configuration as first web-app (api-server). And set in it's settings Docker Command: gunicorn main:web_app --workers 1 --worker-class uvicorn.workers.UvicornWorker --bind=0.0.0.0:8000.

    And I had to make some changes in source codes to use right protocols, hosts and ports.

    Commit: c186719

Layered architecture style

Watch original lesson on Youtube

  1. Implementation of changes made in the video

    I decided to use postgres instead of sqlite and store config in .env file. So, my implementation is a little different from original code.

    Commit: f48cc25

  2. A few code corrections (wrong type-hint for argument in TasksService.init(), SQL models and tables naming (should be singular)):

    It turned out that it's not an easy task to make alembic migration if you need to rename a table with PK, FK and sequence.. I didn't manage to find solution. So, my migration will recreate table and all data will be lost.

    Commit: 9c6c449

  3. One of the advantages of repository pattern is decoupling from database and the possibility to quickly change the data store method. For example we want to write unit-tests and store our data in memory instead of using SQLAlchemy.

    Let's check it.

    To do that we have to implement utils.repository.InMemoryRepository class which will substitude utils.repository.SQLAlchemyRepository. But we also have to implement repositories.tasks.TasksInMemoryRepository class which will substitude repositories.tasks.TasksRepository, because repositories.tasks.TasksRepository descendant of the utils.repository.SQLAlchemyRepository class.

    I wouldn't say it looks beautiful..

    Commit: 2b7591d

UnitOfWork pattern with FastAPI

Watch original lesson on Youtube

  1. Implementation of changes made in the video

    Commit: 5306df7

  2. Fix some mistakes:

    Wrong attributes type: 23f891a

    Delete the extra field author_id in the TaskSchemaEdit: 33b4867

    Add missing methods to AbstractRepository: 27bb74b

  3. Make some improvements:

    Rename implementations that related to SQLAlchemy, add SQLA prefix to make things clearer: 4105d83

    Let's hide UnitOfWork under the hood (we don't have to create TasksService instance and pass uow to each method anymore): a107cec

    Reducing code duplication in schema declarations: 2da4f96

Reading Fast API documentation and practicing new things

1. OAuth scopes

Article: https://fastapi.tiangolo.com/advanced/security/oauth2-scopes/

Let's add scopes to authorization methods, implemented before

Commits: c80df85, ca56b21

2. Sub Applications - Mounts

Article: https://fastapi.tiangolo.com/advanced/sub-applications/

I have 2 applications (web_app and api_app) and have to run them separately. It's not very convenient.

Let's mount api_app to web_app with path /api!

FastAPI doesn't use lifespan for subapplications, so I combined initialization steps in one lifespan and use them for both apps.

Now we can run these applications either separately or together depends on settings (if host and port is the same for web_app and api_app, then api_app will be mounted as subapp).

Commit: c2e5583

3. Testing WebSockets

Article: https://fastapi.tiangolo.com/advanced/testing-websockets/

Let's test our chat. Commit: 2bb68de

There is a problem. If something is wrong in your route function and server doesn't send anything to client, test will be blocked in a deadloop. Commit: 77d3000 (Here we forward text only to client, who sent this message, but don't forward it to other connected clients). To solve this problem it's needed to set timeout when self._send_queue.get() is called in starlette.testclient.WebSocketTestSession.receive(). I'll add an issue to starlette repository.

4. OAuth2 with refresh tokens + rotation

Article: https://stateful.com/blog/oauth-refresh-token-best-practices

To make app more sequre it's better to set short lifetime for access tokens and user refresh token to get new access token. Along with refresh token rotetion to make it even more secure.

Commit: 056395e

Several known disadvantages of this implementation: 1) it will work if only each user use one connection, 2) if malicious user steal the refresh token, they can block the ability of user to work with system until stolen token expired.

I think that writing your own authorization methods is not the best solution. It's better to use proven library instead. I'm going to try integration FastAPI with keycloak later. Update: Done

5. Request rate limit

Article: https://www.moesif.com/blog/technical/rate-limiting/Best-Practices-for-API-Rate-Limits-and-Quotas-With-Moesif-to-Avoid-Angry-Customers/

Rate limit with slowapi: commit 868887f

6. SQLModel

Article: https://sqlmodel.tiangolo.com/

Let's practice using SQLModel library and refactor oauth2 methods to use SQLModel. Commit: 581ff8f

And let's add more complex models: Hero and Team with m2m relations. Commit: 148b8fd

7. Monitoring FastAPI with Prometheus and Grafana

Article: https://dev.to/ken_mwaura1/getting-started-monitoring-a-fastapi-app-with-grafana-and-prometheus-a-step-by-step-guide-3fbn

Commit: 450e979

There is a problem: when you run app with guvicorn then counters will be broken (every worker will have their own counter variables).

To solve this problem you should create in your app work folder empty folder with name tmp_multiproc before starting your app (or clear this folder if it already exists) and add enviroument variable PROMETHEUS_MULTIPROC_DIR=/tmp_multiproc. This looks bad but it's official solution.

Commit: 103f958

8. Authentication with Keycloak

Implementation of direct access grants flow. User (front-end) authenticates on Keycloak server and uses token to access protected FastAPI endpoints.

The main advantage of this approach is that we don't need to create any user managment endpoints, we delegate all of this stuff to Keycloak (which does it securely and provides a user-friendly and flexible UI).

Commit: ff6770

About

Here I'm practicing using FastAPI while taking online course (https://www.youtube.com/playlist?list=PLeLN0qH0-mCVQKZ8-W1LhxDcVlWtTALCS). BTW i'm trying to improve the code from the course. Thanks to Artem Shumeiko for this course!

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages