Potential Improvements
ThirVondukr opened this issue · 9 comments
First of all - it's an amazing set of best practices, I also want to share some things that I use:
Project Structure
src
could be added to PYTHONPATH
to avoid
prefixing every app import with src
, IDEs like PyCharm also support that.
Models also could be stored in same package, it's easier to import your models and make sure all modules were executed when you generate your migrations:
src/
db/
models/
__init__.py
comments.py
posts.py
tags.py
base.py
dependencies.py
# __init__.py
from . commends import Comment
from . posts import Post
from . tags import Tag
__all__ = ["Comment", "Post", "Tag"]
# Anywhere in the code
from db.models import Post
Continuous Integration
Absolutely use CI in gitlab/github to automate your tests and linters!
Dependency Management
Use poetry instead of requirements.txt, it's awesome!
Custom base model from day 0
This could also be used to enable orm_mode
and set up custom alias_generator
if client (for example a JS app) requires it.
Use Starlette's Config object Pydantic BaseSettings!
Pydantic has its own class to manage environment variables:
class AppSettings(BaseSettings):
class Config:
env_prefix = "app_"
domain: str
Hey, @ThirVondukr
Thank you for your kind words, I truly appreciate them!
- Although I find this suggestion very handy, I am not sure to advocate implicit solutions like PYTHONPATH modifications or
__init__
imports. We did put some models there when it was appropriate though, just not an "every package" practice I believe. - I agree, that CI is very important, but it feels like that recommendation doesn't fall into FastAPI best practices in particular.
- poetry is great, but I wish it would be easier to use it in docker containers :)
- Agree, I will add these suggestions.
- Agree, too many people were pointing it out. My key point was not to use other libraries but already installed ones though.
Storing db models in same place is just a personal preference, but I feel like it's very usable in small applications/microservices.
You can use poetry in docker, either by exporting your dependencies as requirements.txt
in a multi-stage build or directly:
FROM python:3.10-slim as build
ENV POETRY_VERSION=1.1.14
RUN pip install poetry==$POETRY_VERSION
COPY ./pyproject.toml ./poetry.lock ./
RUN poetry export -f requirements.txt -o requirements.txt
FROM python:3.10-slim as final
WORKDIR /app
ENV PYTHONPATH=$PYTHONPATH:src
COPY --from=build requirements.txt .
RUN pip install --upgrade pip && pip install -r requirements.txt --no-cache-dir
COPY ./src ./src
COPY alembic.ini ./
...
Yep, agree. We actually do store all DB models in one folder in a small service we have for transcoding.
Well, your example with multi-stage looks pretty neat and clean! I didn't like the examples I have seen online, but will definitely try your example.
I'd say there's also a "problem" with using sqlalchemy commits with fastapi.
Usually you should use .flush()
method to flush sql to database if you need a server-side generated data such as primary keys, you still can use nested transactions, but you don't need to in most cases.
If you use a typical get_session
dependency and commit inside of it then commit would actually happen after the response is sent, which can lead to client receiving 200/201 for example but data won't actually be saved:
async def get_session() -> AsyncIterable[AsyncSession]:
async with async_sessionmaker.begin() as session:
yield session
This could be avoided by either
- Committing manually in each request
- Using another framework/library to do DI for you
- (Best Option) Using a middleware to commit session in it
I think that also could be covered in this repo
Storing db models in same place is just a personal preference, but I feel like it's very usable in small applications/microservices.
You can use poetry in docker, either by exporting your dependencies as
requirements.txt
in a multi-stage build or directly:FROM python:3.10-slim as build ENV POETRY_VERSION=1.1.14 RUN pip install poetry==$POETRY_VERSION COPY ./pyproject.toml ./poetry.lock ./ RUN poetry export -f requirements.txt -o requirements.txt FROM python:3.10-slim as final WORKDIR /app ENV PYTHONPATH=$PYTHONPATH:src COPY --from=build requirements.txt . RUN pip install --upgrade pip && pip install -r requirements.txt --no-cache-dir COPY ./src ./src COPY alembic.ini ./ ...
I agree with the usage of poetry in combination with docker multi stage, this behavior is also suggested by official documentation.
Another thing I usually do is to create a install-poetry.sh
script with the poetry version specified inside, in this way there is only one point of trust used by both Docker and the Developer.
Then simply include it into the docker first stage:
RUN bash install-poetry.sh
RUN poetry export -f requirements.txt --output requirements.txt --without-hashes
First of all - it's an amazing set of best practices, I also want to share some things that I use:
Project Structure
src
could be added toPYTHONPATH
to avoid prefixing every app import withsrc
, IDEs like PyCharm also support that.Models also could be stored in same package, it's easier to import your models and make sure all modules were executed when you generate your migrations:
src/ db/ models/ __init__.py comments.py posts.py tags.py base.py dependencies.py
# __init__.py from . commends import Comment from . posts import Post from . tags import Tag __all__ = ["Comment", "Post", "Tag"]# Anywhere in the code from db.models import PostContinuous Integration
Absolutely use CI in gitlab/github to automate your tests and linters!
Dependency Management
Use poetry instead of requirements.txt, it's awesome!
Custom base model from day 0
This could also be used to enable
orm_mode
and set up customalias_generator
if client (for example a JS app) requires it.Use
Starlette's Config objectPydantic BaseSettings!Pydantic has its own class to manage environment variables:
class AppSettings(BaseSettings): class Config: env_prefix = "app_" domain: str
I am using the method described here as a way to split secrets over different environments with pydantic settings:
https://rednafi.github.io/digressions/python/2020/06/03/python-configs.html
@b-bokma I'm currently using kubernetes and it's really easy to store your k8s secrets and configmaps in same format as your pydantic models:
class DatabaseSettings(BaseSettings):
class Config:
env_prefix = "database_"
driver: str = "postgresql+asyncpg"
sync_driver = "postgresql+psycopg2"
name: str
username: str
password: str
host: str
apiVersion: apps/v1
kind: Deployment
metadata:
...
spec:
...
template:
...
spec:
...
containers:
- name: fastapi
...
envFrom:
- secretRef:
name: app-database
prefix: database_
This also helps if you need to use same secret for different applications
So we have a different sets of secrets in different namespaces at this moment 😉
Hey, @ThirVondukr
Thank you for your kind words, I truly appreciate them!
- Although I find this suggestion very handy, I am not sure to advocate implicit solutions like PYTHONPATH modifications or
__init__
imports. We did put some models there when it was appropriate though, just not an "every package" practice I believe.- I agree, that CI is very important, but it feels like that recommendation doesn't fall into FastAPI best practices in particular.
- poetry is great, but I wish it would be easier to use it in docker containers :)
- Agree, I will add these suggestions.
- Agree, too many people were pointing it out. My key point was not to use other libraries but already installed ones though.
I don't understand why not to use poetry right in docker?
It is possible and very convenient to use the groups of dependencies provided by poetry?
The workflow is the following:
- install poetry tool by pip (the problem can only be in the size of this tool)
- copy pyproject.toml and poetry.lock files
- set env variable
POETRY_VIRTUALENVS_CREATE
to false, not to create env in docker container - run
poetry install --only main --no-root
@PashaDem Because I personally don't think you need an extra dependency/tool inside of your docker container, if you need it for something - add it to your docker image.