From ecc3afeef3b80c7c49e3b724f18b1aea6dbaeac4 Mon Sep 17 00:00:00 2001 From: Yerassyl Date: Fri, 6 Jan 2023 01:19:21 +0600 Subject: [PATCH 1/6] Fix typos and put sample link under contents (#14) --- README.md | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 79e7f83..4fcd460 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,6 @@ For the last 1.5 years in production, we have been making good and bad decisions that impacted our developer experience dramatically. Some of them are worth sharing. -Project [sample](https://github.com/zhanymkanov/fastapi_production_template) built with this best-practices in mind. - ### Contents 1. [Project Structure. Consistent & predictable.](https://github.com/zhanymkanov/fastapi-best-practices#1-project-structure-consistent--predictable) 2. [Excessively use Pydantic for data validation.](https://github.com/zhanymkanov/fastapi-best-practices#2-excessively-use-pydantic-for-data-validation) @@ -33,6 +31,7 @@ Project [sample](https://github.com/zhanymkanov/fastapi_production_template) bui 23. [If you must use sync SDK, then run it in a thread pool.](https://github.com/zhanymkanov/fastapi-best-practices#23-if-you-must-use-sync-sdk-then-run-it-in-a-thread-pool) 24. [Use linters (black, isort, autoflake).](https://github.com/zhanymkanov/fastapi-best-practices#24-use-linters-black-isort-autoflake) 25. [Bonus Section.](https://github.com/zhanymkanov/fastapi-best-practices#bonus-section) +

Project sample built with these best-practices in mind.

### 1. Project Structure. Consistent & predictable There are many ways to structure the project, but the best structure is a structure that is consistent, straightforward, and has no surprises. @@ -141,7 +140,7 @@ class UserBase(BaseModel): ``` ### 3. Use dependencies for data validation vs DB -Pydantic can only validate the values of client input. +Pydantic can only validate the values from client input. Use dependencies to validate data against database constraints like email already exists, user not found, etc. ```python3 # dependencies.py @@ -214,7 +213,6 @@ async def valid_owned_post( # router.py @router.get("/users/{user_id}/posts/{post_id}", response_model=PostResponse) async def get_user_post(post: Mapping = Depends(valid_owned_post)): - """Get post that belong the user.""" return post ``` @@ -328,12 +326,12 @@ async def get_user_profile_by_id(profile: Mapping = Depends(valid_profile_id)): async def get_user_profile_by_id( creator_profile: Mapping = Depends(valid_creator_id) ): - """Get profile by id.""" + """Get creator's profile by id.""" return creator_profile ``` -Use /me endpoints for users own resources (e.g. `GET /profiles/me`, `GET /users/me/posts`) +Use /me endpoints for users resources (e.g. `GET /profiles/me`, `GET /users/me/posts`) 1. No need to validate that user id exists - it's already checked via auth method 2. No need to check whether the user id belongs to the requester @@ -367,8 +365,8 @@ def good_ping(): @router.get("/perfect-ping") async def perfect_ping(): - await asyncio.sleep(10) # non I/O blocking operation - pong = await service.async_get_pong() # non I/O blocking db call + await asyncio.sleep(10) # non-blocking I/O operation + pong = await service.async_get_pong() # non-blocking I/O db call return {"pong": pong} @@ -390,6 +388,7 @@ async def perfect_ping(): 3. While `good_ping` is being executed, event loop selects next tasks from the queue and works on them (e.g. accept new request, call db) - Independently of main thread (i.e. our FastAPI app), worker thread will be waiting for `time.sleep` to finish and then for `service.get_pong` to finish + - Sync operation blocks only the side thread, not the main one. 4. When `good_ping` finishes its work, server returns a response to the client 3. `GET /perfect-ping` 1. FastAPI server receives a request and starts handling it @@ -399,7 +398,7 @@ async def perfect_ping(): 5. Event loop selects next tasks from the queue and works on them (e.g. accept new request, call db) 6. When `service.async_get_pong` is done, server returns a response to the client -The second caveat is that operations that are non-blocking awaitables or are sent to thread pool must be I/O intensive tasks (e.g. open file, db call, external API call). +The second caveat is that operations that are non-blocking awaitables or are sent to the thread pool must be I/O intensive tasks (e.g. open file, db call, external API call). - Awaiting CPU-intensive tasks (e.g. heavy calculations, data processing, video transcoding) is worthless since the CPU has to work to finish the tasks, while I/O operations are external and server does nothing while waiting for that operations to finish, thus it can go to the next tasks. - Running CPU-intensive tasks in other threads also isn't effective, because of [GIL](https://realpython.com/python-gil/). @@ -596,7 +595,7 @@ async def test_create_post(client: TestClient): Unless you have sync db connections (excuse me?) or aren't planning to write integration tests. ### 15. BackgroundTasks > asyncio.create_task BackgroundTasks can [effectively run](https://github.com/encode/starlette/blob/31164e346b9bd1ce17d968e1301c3bb2c23bb418/starlette/background.py#L25) -both blocking and non-blocking I/O operations the same way it handles routes (`sync` functions are run in a threadpool, while `async` ones are awaited later) +both blocking and non-blocking I/O operations the same way FastAPI handles blocking routes (`sync` tasks are run in a threadpool, while `async` tasks are awaited later) - Don't lie to the worker and don't mark blocking I/O operations as `async` - Don't use it for heavy CPU intensive tasks. ```python @@ -666,9 +665,9 @@ print(type(post.content)) # Article is very inclusive and all fields are optional, allowing any dict to become valid ``` **Solutions:** -1. Validate input has only valid fields +1. Validate input has only allowed valid fields and raise error if unknowns are provided ```python -from pydantic import BaseModel, Extra, root_validator +from pydantic import BaseModel, Extra class Article(BaseModel): text: str | None @@ -733,7 +732,7 @@ print(type(p.content)) # OUTPUT: Article, because smart_union doesn't work for complex fields like classes ``` -**Fast Workaround:** +3. Fast Workaround Order field types properly: from the most strict ones to loose ones. @@ -950,7 +949,7 @@ async def root(): [INFO] [2022-08-28 12:00:00.000030] called dict ``` ### 23. If you must use sync SDK, then run it in a thread pool. -If you must use an SDK to interact with external services, and it's not `async`, +If you must use a library to interact with external services, and it's not `async`, then make the HTTP calls in an external worker thread. For a simple example, we could use our well-known `run_in_threadpool` from starlette. From eb44489578e5adce04e295a26475568c1fdd275f Mon Sep 17 00:00:00 2001 From: nbanic Date: Mon, 9 Jan 2023 11:54:57 +0100 Subject: [PATCH 2/6] Fixing a link. (#15) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4fcd460..d4b8cc9 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Some of them are worth sharing. 14. [Set tests client async from day 0.](https://github.com/zhanymkanov/fastapi-best-practices#14-set-tests-client-async-from-day-0) 15. [BackgroundTasks > asyncio.create_task.](https://github.com/zhanymkanov/fastapi-best-practices#15-backgroundtasks--asynciocreate_task) 16. [Typing is important.](https://github.com/zhanymkanov/fastapi-best-practices#16-typing-is-important) -17. [Save files in chunk.](https://github.com/zhanymkanov/fastapi-best-practices#17-save-files-in-chunk) +17. [Save files in chunks.](https://github.com/zhanymkanov/fastapi-best-practices#17-save-files-in-chunks) 18. [Be careful with dynamic pydantic fields.](https://github.com/zhanymkanov/fastapi-best-practices#18-be-careful-with-dynamic-pydantic-fields) 19. [SQL-first, Pydantic-second.](https://github.com/zhanymkanov/fastapi-best-practices#19-sql-first-pydantic-second) 20. [Validate hosts, if users can send publicly available URLs.](https://github.com/zhanymkanov/fastapi-best-practices#20-validate-hosts-if-users-can-send-publicly-available-urls) From c2b45d4f8ee2f5d9add6d924bd7550b60366eb61 Mon Sep 17 00:00:00 2001 From: tangkikodo Date: Mon, 13 Mar 2023 12:29:11 +0800 Subject: [PATCH 3/6] Fix typos --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d4b8cc9..061a039 100644 --- a/README.md +++ b/README.md @@ -887,7 +887,7 @@ class Profile(BaseModel): ``` ### 21. Raise a ValueError in custom pydantic validators, if schema directly faces the client -It wil return a nice detailed response to users. +It will return a nice detailed response to users. ```python # src.profiles.schemas from pydantic import BaseModel, validator From 631193754953631c805a863e00af99091ff1af5a Mon Sep 17 00:00:00 2001 From: sunn <110405224+morning-sunn@users.noreply.github.com> Date: Sun, 9 Apr 2023 08:22:04 +0200 Subject: [PATCH 4/6] Fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 061a039..38361a5 100644 --- a/README.md +++ b/README.md @@ -753,7 +753,7 @@ from pydantic import UUID4 from sqlalchemy import desc, func, select, text from sqlalchemy.sql.functions import coalesce -from src.database import databse, posts, profiles, post_review, products +from src.database import database, posts, profiles, post_review, products async def get_posts( creator_id: UUID4, *, limit: int = 10, offset: int = 0 From 00704cc2ae03820c2a169529e3912b6ec77e59da Mon Sep 17 00:00:00 2001 From: Daniil Okhlopkov <5613295+ohld@users.noreply.github.com> Date: Thu, 29 Jun 2023 18:09:10 +0100 Subject: [PATCH 5/6] Fix typos --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 38361a5..298fb26 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ fastapi-project 6. `constants.py` - module specific constants and error codes 7. `config.py` - e.g. env vars 8. `utils.py` - non-business logic functions, e.g. response normalization, data enrichment, etc. - 9. `exceptions` - module specific exceptions, e.g. `PostNotFound`, `InvalidUserData` + 9. `exceptions.py` - module specific exceptions, e.g. `PostNotFound`, `InvalidUserData` 3. When package requires services or dependencies or constants from other packages - import them with an explicit module name ```python from src.auth import constants as auth_constants @@ -439,7 +439,7 @@ class ORJSONModel(BaseModel): class Config: json_loads = orjson.loads json_dumps = orjson_dumps - json_encoders = {datetime: convert_datetime_to_gmt} # method for customer JSON encoding of datetime fields + json_encoders = {datetime: convert_datetime_to_gmt} # method for custom JSON encoding of datetime fields @root_validator() def set_null_microseconds(cls, data: dict) -> dict: From db781d6b5415a48318046723701da3c27771840b Mon Sep 17 00:00:00 2001 From: Daliseiy <44264075+Daliseiy@users.noreply.github.com> Date: Tue, 4 Jul 2023 19:08:59 +0100 Subject: [PATCH 6/6] Fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 298fb26..439f218 100644 --- a/README.md +++ b/README.md @@ -302,7 +302,7 @@ also checks if the profile is creator, then it's better to rename `creator_id` p ```python3 # src.profiles.dependencies async def valid_profile_id(profile_id: UUID4) -> Mapping: - profile = await service.get_by_id(post_id) + profile = await service.get_by_id(profile_id) if not profile: raise ProfileNotFound()