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!
-
Lesson 5 (user registration and authentification with fastapi-users)
-
Lesson 11 (Linking Frontend and Backend. Cors and Middleware)
-
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
Watch original lesson on Youtube
-
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
-
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
andauth.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 inmodels.model.py
and inherit fromSQLAlchemyBaseUserTable
. Also, new version offastapi-users
documentation follows the orm style to declare this table. So, let's use the same style inmodels.py
.Commit: 5034ae2
-
It's said in course that
SECRETS
forfastapi-users
should be stored in.env
. Move them to.env
.Commit: fa9517c
-
I don't like the idea of overriding the
create
method inUserManager
class just to set defaultrole_id
value. We can easily do it by specifying thedefault
parameter inUser
model and removingrole_id
from theUserCreate
schema.Commit: 8a2269c
-
In addition to 3 already implemented API endpoints (
register
,login
,logout
), let's implementforgot_password
andupdate
endpoints.Commits: 8890f19 and eee3d60
-
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 useBearer
transport andDatabase
strategy.Commit: c9369cb
Watch original lesson on Youtube
-
Changing project structure and adding
operations
module as it's shown in lesson's source code.Commit: bc4f100
-
Using one metadata object for all database models, using ORM-style to declare
operations
table. Getting rid of depricateddict
method inoperations.router
. Renamingbase_config
tobackends
. -
Grouping
auth
routes into modulerouter
and include this router inmain
.Commit: 6fbd3fd
-
Configuring the migration file name format to include date and time.
Commit: bfb7e1c
Watch original lesson on Youtube
-
Implementation of changes made in the lesson #7
Commit: c8b1b78
-
Doing lesson's homework #1 (using HTTP PUT and PATCH methods)
Commit: 27fab33
-
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
anddetail
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 additionalstatus
field. The response to a successful request will contain only data (at the first level, without the additionaldata
field), for unsuccessful requests it will contain thedetail
field (as it is done infatsapi-users
).In addition, let's specify the response schemes for the endpoints of the
operations
module. -
Doing lesson's homework #3 (pagination of results)
Commit: fb0c767
-
Catching
database error
,doesn't exist
andalready exist
errors. Moving the handling of common errors outside of endpoint functions.Commit: a0ecbdd
-
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 ofPOST
andPUT
methods. NowPOST
will not acceptid
(it will be autogenerated) andPUT
will return error if record with requestedid
doesn't exist (before these changes thePUT
method created new record if record with requestedid
doesn't exist).Commit: 3d2f69e
Watch original lesson on Youtube
-
Implementation of changes made in the lesson #8
Commit: 8b28197
-
Let's try to use
fastapi-cache
withInMemoryBackend
. Note thatInMemoryBackend
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
-
Let's learn how to manage client-side caching by setting headers.
Commit: cb97d4f
-
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 addCache-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 addingresponse.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..
Watch original lesson on Youtube
-
Implementation of changes made in the lesson #9
Commit: 2d0f98f
-
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
-
The use of FastAPI's
on_startup
andon_shoutdown
events is deprecated, we should uselifespan
instead.Commit: fd61e6b
-
Adding celery-task execution monitoring endpoins.
Commit: ce7f425
-
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
Watch original lesson on Youtube
-
Implementation of changes made in the lesson #10
Commit: ff80cfe
-
Understanding the neccessety of the
event_loop
fixture.As I learned from the
python-asyncio
documentation, we need to override defaultevent_loop
fixture if we use fixtures with the scope different tofunction
wich is the default scope. So, since we use fixtures withsession
scope, we need to overrideevent_loop
fixture with the same scope (it might be any scope that is equal to or wider than others). -
Testing endpoints, that use '@cache' decorator.
As
httpx.AsyncClient
doesn't implement thelifespan
protocol, it doesn't use (evoke)lifespan
context wherefastapi-cache
is initialized. The solution is to add just one line of code in theac
fixture.Commit: 6b10367
-
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
Watch original lesson on Youtube
-
Implementation of changes made in the lesson #11
Commit: c7aa64d
-
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"]
, openedhttps://fastapi.tiangolo.com
in browser, then openedinspect
->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.
Watch original lesson on Youtube
-
Implementation of changes made in the lesson #12
Commit: 3e96cf7
-
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
-
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
Watch original lesson on Youtube
-
Implementation of changes made in the lesson #13
Commit: c098aec
-
Some code refinements:
-
Move
ConnectionManager
to separate file, rename it toChatManager
-
I think the "ChatManager" should encapsulate all
chat
logic, let's move all database operations to its methods and passsession
as a parameter. -
Getting rid of underscores in URL (it's recomended to use dashes instead)
-
-
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
-
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
-
Alternatives of Websockets.
Watch original lesson on Youtube
-
Implementation of changes made in the lesson #14
Commit: f4b6d11
-
It's recommended in the FastAPI documentation to use
Annotated
instead of passingDepends
as a default attribute value.Commit: 00734e4
-
Let's make oauth2 example more secure by following the FastAPI documentation
Commit: 5003291
-
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 theyield
.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)).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 raiseHTTPException
in dependencies after yield.Commit: 73a36eb
Watch original lesson on Youtube
-
Implementation of changes made in the lesson #15
Commit: 0b1edeb
-
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
-
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
-
This version of docker-compose file runs only api-server. But we need to run web-server too.
Commit: 595ac10
Watch original lesson on Youtube
-
Implementation of changes made in the lesson #16
-
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.comDockerfile 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 makedocker-compose
solution work again.Commit: 662899b
-
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
Watch original lesson on Youtube
-
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
-
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
-
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 substitudeutils.repository.SQLAlchemyRepository
. But we also have to implementrepositories.tasks.TasksInMemoryRepository
class which will substituderepositories.tasks.TasksRepository
, becauserepositories.tasks.TasksRepository
descendant of theutils.repository.SQLAlchemyRepository
class.I wouldn't say it looks beautiful..
Commit: 2b7591d
Watch original lesson on Youtube
-
Implementation of changes made in the video
Commit: 5306df7
-
Fix some mistakes:
Wrong attributes type: 23f891a
Delete the extra field
author_id
in theTaskSchemaEdit
: 33b4867Add missing methods to
AbstractRepository
: 27bb74b -
Make some improvements:
Rename implementations that related to
SQLAlchemy
, addSQLA
prefix to make things clearer: 4105d83Let's hide
UnitOfWork
under the hood (we don't have to createTasksService
instance and passuow
to each method anymore): a107cecReducing code duplication in schema declarations: 2da4f96
Article: https://fastapi.tiangolo.com/advanced/security/oauth2-scopes/
Let's add scopes to authorization methods, implemented before
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
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.
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
Rate limit with slowapi
: commit 868887f
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
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
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