Giter Site home page Giter Site logo

teamhide / fastapi-boilerplate Goto Github PK

View Code? Open in Web Editor NEW
879.0 8.0 138.0 504 KB

FastAPI boilerplate for real world production

Python 99.18% Mako 0.71% Makefile 0.11%
fastapi python boilerplate api sqlalchemy asyncio fastapi-template fastapi-boilerplate sqlalchemy-async

fastapi-boilerplate's Introduction

FastAPI Boilerplate

Features

  • Async SQLAlchemy session
  • Custom user class
  • Dependencies for specific permissions
  • Celery
  • Dockerize(Hot reload)
  • Event dispatcher
  • Cache

Run

Launch docker

> docker-compose -f docker/docker-compose.yml up

Install dependency

> poetry shell
> poetry install

Apply alembic revision

> alembic upgrade head

Run server

> python3 main.py --env local|dev|prod --debug

Run test codes

> make test

Make coverage report

> make cov

Formatting

> pre-commit

SQLAlchemy for asyncio context

from core.db import Transactional, session


@Transactional()
async def create_user(self):
    session.add(User(email="[email protected]"))

Do not use explicit commit(). Transactional class automatically do.

Query with asyncio.gather()

When executing queries concurrently through asyncio.gather(), you must use the session_factory context manager rather than the globally used session.

from core.db import session_factory


async def get_by_id(self, *, user_id) -> User:
    stmt = select(User)
    async with session_factory() as read_session:
        return await read_session.execute(query).scalars().first()


async def main() -> None:
    user_1, user_2 = await asyncio.gather(
        get_by_id(user_id=1),
        get_by_id(user_id=2),
    )

If you do not use a database connection like session.add(), it is recommended to use a globally provided session.

Multiple databases

Go to core/config.py and edit WRITER_DB_URL and READER_DB_URL in the config class.

If you need additional logic to use the database, refer to the get_bind() method of RoutingClass.

Custom user for authentication

from fastapi import Request


@home_router.get("/")
def home(request: Request):
    return request.user.id

Note. you have to pass jwt token via header like Authorization: Bearer 1234

Custom user class automatically decodes header token and store user information into request.user

If you want to modify custom user class, you have to update below files.

  1. core/fastapi/schemas/current_user.py
  2. core/fastapi/middlewares/authentication.py

CurrentUser

class CurrentUser(BaseModel):
    id: int = Field(None, description="ID")

Simply add more fields based on your needs.

AuthBackend

current_user = CurrentUser()

After line 18, assign values that you added on CurrentUser.

Top-level dependency

Note. Available from version 0.62 or higher.

Set a callable function when initialize FastAPI() app through dependencies argument.

Refer Logging class inside of core/fastapi/dependencies/logging.py

Dependencies for specific permissions

Permissions IsAdmin, IsAuthenticated, AllowAll have already been implemented.

from core.fastapi.dependencies import (
    PermissionDependency,
    IsAdmin,
)


user_router = APIRouter()


@user_router.get(
    "",
    response_model=List[GetUserListResponseSchema],
    response_model_exclude={"id"},
    responses={"400": {"model": ExceptionResponseSchema}},
    dependencies=[Depends(PermissionDependency([IsAdmin]))],  # HERE
)
async def get_user_list(
    limit: int = Query(10, description="Limit"),
    prev: int = Query(None, description="Prev ID"),
):
    pass

Insert permission through dependencies argument.

If you want to make your own permission, inherit BasePermission and implement has_permission() function.

Note. In order to use swagger's authorize function, you must put PermissionDependency as an argument of dependencies.

Event dispatcher

Refer the README of https://github.com/teamhide/fastapi-event

Cache

Caching by prefix

from core.helpers.cache import Cache


@Cache.cached(prefix="get_user", ttl=60)
async def get_user():
    ...

Caching by tag

from core.helpers.cache import Cache, CacheTag


@Cache.cached(tag=CacheTag.GET_USER_LIST, ttl=60)
async def get_user():
    ...

Use the Cache decorator to cache the return value of a function.

Depending on the argument of the function, caching is stored with a different value through internal processing.

Custom Key builder

from core.helpers.cache.base import BaseKeyMaker


class CustomKeyMaker(BaseKeyMaker):
    async def make(self, function: Callable, prefix: str) -> str:
        ...

If you want to create a custom key, inherit the BaseKeyMaker class and implement the make() method.

Custom Backend

from core.helpers.cache.base import BaseBackend


class RedisBackend(BaseBackend):
    async def get(self, key: str) -> Any:
        ...

    async def set(self, response: Any, key: str, ttl: int = 60) -> None:
        ...

    async def delete_startswith(self, value: str) -> None:
        ...

If you want to create a custom key, inherit the BaseBackend class and implement the get(), set(), delete_startswith() method.

Pass your custom backend or keymaker as an argument to init. (/app/server.py)

def init_cache() -> None:
    Cache.init(backend=RedisBackend(), key_maker=CustomKeyMaker())

Remove all cache by prefix/tag

from core.helpers.cache import Cache, CacheTag


await Cache.remove_by_prefix(prefix="get_user_list")
await Cache.remove_by_tag(tag=CacheTag.GET_USER_LIST)

fastapi-boilerplate's People

Contributors

dependabot[bot] avatar teamhide avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

fastapi-boilerplate's Issues

안녕하세요 TeamHide님, 질문이 있습니다.

기존에 작성 하셨던 stand_alone_session 에 DI 를 달아서 FastAPI BackgroundTask 용으로 사용하고 있습니다.

# Di
    writer_engine = providers.Singleton(
        create_async_engine,
        config.WRITER_DB_URL,
        pool_recycle=3600,
        connect_args={"server_settings": {"jit": "off"}},
    )
    reader_engine = providers.Singleton(
        create_async_engine,
        config.READER_DB_URL,
        pool_recycle=3600,
        connect_args={"server_settings": {"jit": "off"}},
    )
    async_session_factory = providers.Singleton(
        sessionmaker,
        class_=AsyncSession,
        autocommit=False,
        autoflush=True,
        expire_on_commit=False,
        sync_session_class=RoutingSession,
    )
    session = providers.Singleton(
        async_scoped_session,
        session_factory=async_session_factory,
        scopefunc=get_session_context,
    )
from uuid import uuid4
from dependency_injector.wiring import Provide
from sqlalchemy.ext.asyncio import async_scoped_session
from .session_maker import reset_session_context, set_session_context

session: async_scoped_session = Provide["session"]

def standalone_session(func):
    async def _standalone_session(*args, **kwargs):
        session_id = str(uuid4())
        context = set_session_context(session_id=session_id)

        try:
            result = await func(*args, **kwargs)
            await session.commit()
        except Exception as e:
            await session.rollback()
            raise e
        finally:
            await session.remove()
            reset_session_context(context=context)
        return result

    return _standalone_session
@standalone_session
@inject
async def delete_all_user_data(user_id: UUID, user_oauth_id: UUID, session: async_scoped_session = Provide["session"]):
    """
    사용자의 모든 데이터를 삭제합니다.
    """
    await session.execute("DELETE FROM item_orders WHERE user_id = :user_id;", {"user_id": user_id})
    ....

기 작성 되었던 Transactional 과 비슷하다고 생각해서
기존에 try 블록에 await session.commit() 가 빠졌구나 하고 추가해서 쓰고 있었습니다.

오늘 코드 짜다가 func 내 session 에서 commit 하지 않고, standalone_session 데코레이터에 있는 commit으로 커밋 되겠거니 하고 넘겼습니다.
커밋이 안되었고, 예전에 왜 되었지 하고 쓴것들보니 다 func내에서 .commit()을 하고 있었네요.
func내에 있는 session과 standalone_session에 있는 session의 .id() 확인 해보니 안되는게 맞는것 같기도 합니다.

session 객체를 동일하게 사용하려고, provide.Singleton을 사용한건데
제가 무엇을 잘못 이해하고 있는지 답변을 부탁드려도 될까요?

What is the appropriate way to log?

I want to add some logs(INFO, WARNING, etc), so are there some reference that how to log in this boilerplate that how to initialize logger and do some print?

What is the config for postgres ?

Hi,
Can you guide me how can I change config for postgres ? I am using postgres+pyscopg2://postgres:password@localhost:5432/fastapi. But I have no luck ?

Config file not working?

Hi.
In the main.py file there is an import from core.config import config, which means that get_config() is being executed before os.environ["ENV"] = env from main(), so I always end up with "development" from ENV: str = "development".
Am I missing something?

How get currentUser

I'm call CurrentUser class, and get null result, but I authorized in app. If print CurrentUser in AuthBackend, I'm see my ID and if call request.user.id I'm too see my id. How fix it?

Response Model에 대한 질문

안녕하세요 영식님!

우선, 이런 좋은 boilerplate를 공유해주신 것에 대해 감사의 말씀 드립니다. github 뿐만 아니라 블로그를 통해서도 정말 많은 도움 받았습니다!

다름이 아니고, endpoint에서 response_model로 adapter.input.api.v1.response 모듈과 application.dto 모듈 둘 다 사용하시던데, 어떤 기준으로 두 가지를 나눠서 사용했는지 알 수 있을까요?

response 모듈은 'API 응답', dto 모듈은 '애플리케이션 내부 데이터 전송 개체 정의'로 알고 있는데, 혹시 어떤 의도로 작성하신 걸까요?

What is the best practice for using this?

Possible stupid question..

Is it as simple as just cloning, editing and using in my new repo?

I was wondering about how to keep my project up to date with best practices from this boilerplate.

Benchmarking strategy is wrong here....

Hey,

I just noticed that you didn't use same driver when benchmarking these ORMs,
so it gave tortoise a not deserved boost!
I believe you should have used all with psycopg2, since all support it as far as I know.
Or maybe perform a sync and async benchmark driver, then compare them. Those who supports async driver usually supports asyncpg, since it's best out there.

프로젝트 구성에 대한 질문

안녕하세요, 올려주신 작업에 감사합니다.
올려주신 것 기반으로 프로젝트 구성해보려고 하고 있는데 궁금한 것이 있어 질문 드립니다.

  1. Api Layer를 app/resource 하위에 다 넣고 (django 처럼) resouce 별 schema를 같이 모으면 더 불편할까요?

  2. BaseRepo 가 있는데 별도로 활용은 되고 있지 않아서 api -> service -> repository 로 의도하신거라고 보면 될까요?

비밀번호 암호화 기능 추가

@Transactional(propagation=Propagation.REQUIRED)
async def create_user(
self, email: str, password1: str, password2: str, nickname: str
) -> None:
if password1 != password2:
raise PasswordDoesNotMatchException
query = select(User).where(or_(User.email == email, User.nickname == nickname))
result = await session.execute(query)
is_exist = result.scalars().first()
if is_exist:
raise DuplicateEmailOrNicknameException
user = User(email=email, password=password1, nickname=nickname)
session.add(user)

위 소스코드 부분이 사용자를 추가하는 테스트 API 부분인데, 아무래도 비밀번호를 평문으로 저장한다는 것이 좀 걸리네요..

token_helper.py로 JWT 처리에 대한 로직도 있고, AuthBackend를 통해서 Authorization 까지 체크하는 기본적인 보안 기능을 제공하는데 기왕 하는김에 비밀번호 암호화 기능도 추가하는 건 어떤가 싶어 제안해 봅니다. (특히 국내에선 비밀번호 암호화가 법적으로 필수라서 꼭 추가했으면 좋겠네요)

그리고 추가적인 제안인데 JWT 토큰을 사용하는데 API단에서 예시가 없어서 로그인 시 JWT토큰 발급 / Bearer를 통한 JWT 토큰 사용 예시 API까지 추가했으면 하는 바램입니다

Code revision

Hello how are you?

Just tried to use your boilerplate and wanted to thank you for the work.

I've want to know if you've been able to start it from scratch without having errors?

You don't describe for example that you need to run some alembic commands first, you don't have a requierments.txt to install all the dependencies, maybe there something I'm missing with poetry but still you are importing jwt and it seems that jwt doesn't have anymore and *.encode method, but the JWT class does, thus it seems that some of the code is broken.

I wanted to use a boilerplate to start clean an fresh but I think I will lose less time to do it on my own.

Maybe you might want to check back you code a bit. I don't know.

Thank you anyway.

Can't open files in static folder

I'm was mount static folder and can't get images in browser. I'm was see error
Unicodedecodeerror: 'utf-8' codec can't decode byte 0x89 in position 0: invalid start byte
How fix it? I comment block code
elif message.get("type") == "http.response.body": if body := message.get("body"): response_info.body += body.decode("utf8")

in response_log and it work for me.

프로젝트 스켈레톤에 대한 질문

안녕하세요 영식님,

기존에는
Resource에서 API가 분리된 형태였다가,
이번에 리팩토링 하시면서는 다시 Resouce 내에 API가 포함된 형태로 변경되었네요?

저도 이번에 회사 코드 리팩토링을 하는데 어떤 스타일로 갈지 고민 중입니다.
생각나는건,하나로 다 되어 있을 경우 별도 앱으로 떼어내기가 수월하다.
분리가 되어 있을 경우, API 파악이 빠르다.

고시친 이유가 궁금합니다.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.