From a9372fbe45921a999a8d1d511bc9c4b73ab5f4ae Mon Sep 17 00:00:00 2001 From: Yerassyl Zhanymkanov Date: Sun, 28 Aug 2022 22:24:28 +0600 Subject: [PATCH] Fix some grammarly issues --- README.md | 50 ++++++++++++++++++++++++++------------------------ 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index ba04159..76d4d0a 100644 --- a/README.md +++ b/README.md @@ -34,13 +34,13 @@ Some of them are worth sharing. 25. [Bonus Section.](https://github.com/zhanymkanov/fastapi-best-practices#bonus-section) ### 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. +There are many ways to structure the project, but the best structure is a structure that is consistent, straightforward, and has no surprises. - If looking at the project structure doesn't give you an idea of what the project is about, then the structure might be unclear. -- If you have to open packages to understand what modules are located in it, then your structure is unclear. +- If you have to open packages to understand what modules are located in them, then your structure is unclear. - If the frequency and location of the files feels random, then your project structure is bad. - If looking at the module's location and its name doesn't give you an idea of what's inside it, then your structure is very bad. -Although, the project structure, where we separate files by their type (e.g. api, crud, models, schemas) +Although the project structure, where we separate files by their type (e.g. api, crud, models, schemas) presented by [@tiangolo](https://github.com/tiangolo) is good for microservices or projects with fewer scopes, we couldn't fit it into our monolith with a lot of domains and modules. Structure that I found more scalable and evolvable is inspired by Netflix's [Dispatch](https://github.com/Netflix/dispatch) with some little modifications. @@ -108,7 +108,7 @@ fastapi-project 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` -3. When package requires services or dependencies or constants from other packages - import them with explicit module name +3. When package requires services or dependencies or constants from other packages - import them with explicit an module name ```python from src.auth import constants as auth_constants from src.notifications import service as notification_service @@ -118,8 +118,8 @@ from src.posts.constants import ErrorCode as PostsErrorCode # in case we have S ### 2. Excessively use Pydantic for data validation Pydantic has a rich set of features to validate and transform data. -In addition to regular features like required, non-required fields and default data, -it has built-in comprehensive data processing params like regex, enums for limited allowed options, length validation, email validation, etc. +In addition to regular features like required & non-required fields with default values, +it has built-in comprehensive data processing tools like regex, enums for limited allowed options, length validation, email validation, etc. ```python3 from enum import Enum from pydantic import AnyUrl, BaseModel, EmailStr, Field, constr @@ -141,7 +141,7 @@ class UserBase(BaseModel): ``` ### 3. Use dependencies for data validation vs DB Pydantic can only validate the values of client input. -Use dependencies to validate data against database requirements like email already exists, user not found, etc. +Use dependencies to validate data against database constraints like email already exists, user not found, etc. ```python3 # dependencies.py async def valid_post_id(post_id: UUID4) -> Mapping: @@ -219,7 +219,7 @@ async def get_user_post(post: Mapping = Depends(valid_owned_post)): ``` ### 5. Decouple & Reuse dependencies. Dependency calls are cached. Dependencies can be reused multiple times, and they won't be recalculated - FastAPI caches their result by default, -e.g. if we have a dependency which calls service `get_post_by_id`, we won't be visiting DB each time we call this dependency - only the first function call. +i.e. if we have a dependency that calls service `get_post_by_id`, we won't be visiting DB each time we call this dependency - only the first function call. Knowing this, we can easily decouple dependencies onto multiple smaller functions that operate on a smaller scope and are easier to reuse in other routes. For example, in the code below we are using `parse_jwt_data` three times: @@ -339,13 +339,13 @@ Use /me endpoints for users own resources (e.g. `GET /profiles/me`, `GET /users/ ### 7. Don't make your routes async, if you have only blocking I/O operations Under the hood, FastAPI can [effectively handle](https://fastapi.tiangolo.com/async/#path-operation-functions) both async and sync I/O operations. - FastAPI runs `sync` routes in the [threadpool](https://en.wikipedia.org/wiki/Thread_pool) -and blocking I/O operations won't stop [event loop](https://docs.python.org/3/library/asyncio-eventloop.html) +and blocking I/O operations won't stop the [event loop](https://docs.python.org/3/library/asyncio-eventloop.html) from executing the tasks. -- Otherwise, if the route is defined as `async` then it's called regularly via `await` +- Otherwise, if the route is defined `async` then it's called regularly via `await` and FastAPI trusts you to do only non-blocking I/O operations. The caveat is if you fail that trust and execute blocking operations within async routes, -event loop will not be able to run the next tasks until that blocking operation is done. +the event loop will not be able to run the next tasks until that blocking operation is done. ```python import asyncio import time @@ -399,9 +399,9 @@ async def perfect_ping(): 6. When `service.async_get_pong` is done, server returns a response to the client The caveat is that operations that are non-blocking awaitables or sent to 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 CPU has to work to finish the tasks, +- 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/). +- Running CPU-intensive tasks in other threads also isn't effective, because of [GIL](https://realpython.com/python-gil/). In short, GIL allows only one thread to work at a time, which makes it useless for CPU tasks. - If you want to optimize CPU intensive tasks you should send them to workers in another process. @@ -549,9 +549,7 @@ metadata = MetaData(naming_convention=POSTGRES_INDEXES_NAMING_CONVENTION) If your migrations depend on dynamically generated data, make sure the only thing that is dynamic there is the data itself, not its structure. 2. Generate migrations with descriptive names & slugs. Slug is required and should explain the changes. - -Set human-readable file template for new migrations. -We use `*date*_*slug*.py` pattern, e.g. `2022-08-24_post_content_idx.py` +3. Set human-readable file template for new migrations. We use `*date*_*slug*.py` pattern, e.g. `2022-08-24_post_content_idx.py` ``` # alembic.ini file_template = %%(year)d-%%(month).2d-%%(day).2d_%%(slug)s @@ -601,13 +599,15 @@ both blocking and non-blocking I/O operations the same way it handles routes (`s - 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 -from fastapi import BackgroundTasks +from fastapi import APIRouter, BackgroundTasks from pydantic import UUID4 from src.notifications import service as notifications_service -# router.py +router = APIRouter() + + @router.post("/users/{user_id}/email") async def send_user_email(worker: BackgroundTasks, user_id: UUID4): """Send email to user""" @@ -625,7 +625,7 @@ FastAPI, Pydantic, and modern IDEs encourage to take use of type hints. -### 17. Save files in chunk. +### 17. Save files in chunks. Don't hope your clients will send small files. ```python import aiofiles @@ -639,7 +639,7 @@ async def save_video(video_file: UploadFile): await f.write(chunk) ``` ### 18. Be careful with dynamic pydantic fields -If you have a pydantic field that can accept a union of types, be sure validator explicitly knows the difference between those types. +If you have a pydantic field that can accept a union of types, be sure the validator explicitly knows the difference between those types. ```python from pydantic import BaseModel @@ -711,6 +711,7 @@ class Post(BaseModel): It's a good solution if the fields are simple like `int` or `bool`, but it doesn't work for complex fields like classes. +Without Smart Union ```python from pydantic import BaseModel @@ -727,8 +728,9 @@ print(type(p.field_2)) # OUTPUT: int print(type(p.content)) # OUTPUT: Article - - +``` +With Smart Union +```python class Post(BaseModel): field_1: bool | int field_2: int | str @@ -744,7 +746,7 @@ print(p.field_1) print(type(p.field_2)) # OUTPUT: str print(type(p.content)) -# OUTPUT: Article +# OUTPUT: Article, because smart_union doesn't work for complex fields like classes ``` ### 19. SQL-first, Pydantic-second - Usually, database handles data processing much faster and cleaner than CPython will ever do. @@ -954,7 +956,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 have to use an SDK to interact with external service, and it's not `async`, +If you must use an SDK to interact with external services, and it's not `async`, then make the HTTP calls in an external worker thread. For a fast and simple example, we could use our well-known `run_in_threadpool` from starlette.