Fastapi: How do properly add images to linked models?

Created on 28 Jul 2020  路  8Comments  路  Source: tiangolo/fastapi

Hi there!
In accordance with the documentation, I try to link the table with the image to the product table.
In accordance with Nested Models I created
models.py

class Product(Base):
    __tablename__ = 'products'

    id = Column(Integer, primary_key=True)
    name = Column(String, nullable=False)
    description = Column(String(50))

    images = relationship('Image', back_populates='products')

product = Product.__tablename__

class Image(Base):
    __tablename__ = 'images'

    id = Column(Integer, primary_key=True)
    name = Column(String, nullable=False)
    product_id = Column(Integer, ForeignKey('products.id'))
    product = relationship('Product', back_populates='images')

schemas.py

from pydantic import BaseModel, HttpUrl
from typing import Optional, List

class ProductImageSchema(BaseModel):
    url: HttpUrl
    name: str

class ProductSchema(BaseModel):
    name: str
    description: Optional[str] = None
    images: Optional[List[ProductImageSchema]] = None

In accordance with Request Forms and Files

routers.py

@router.post('/add_product/', response_model=ProductSchema, status_code=201)
async def create_product(file: UploadFile = File(...), payload: ProductSchema = Form(...)):
    print(file.content_type)
    _, ext = os.path.splitext(file.filename)
    content = await file.read()
    if file.content_type not in ['image/jpeg', 'image/png']:
        raise HTTPException(status_code=406, detail="Only .jpeg or .png  files allowed")
    file_name = f'{uuid.uuid4().hex}{ext}'
    async with aiofiles.open(os.path.join('/static/product_images', file_name), "wb") as f:
        await f.write(content)
    path_to_img = os.path.abspath(file_name)
    query = product.insert().values(name=payload.name, description=payload.description, image=path_to_img)


    response_object = {
        ....
    }
    return response_object

But when I try to create a new object through the swagger interface, I get

422 | Error: Unprocessable EntityResponse

and print(file.content_type) is not called, so the problem somewhere in the definition of the function
How do I add images to model tables correctly?

answered question

Most helpful comment

databases is not an ORM, check Tortoise ORM and Gino as well. Both have tutorials using FastAPI on their documentation. I'm on my phone, so I will not provide links to them. 馃憖

All 8 comments

The problem relies on the payload: ProductSchema = Form(...), we can't do that. Form doesn't accept pydantic models: https://fastapi.tiangolo.com/tutorial/request-forms/

I've seen some people that wanted this feature, but I'm not sure if there's a reason to why it doesn't exist. Does anyone know?

In this case, how to correctly pass the pydantic model fields as a function argument?

(by the way, this looks like a sync code inside async handler)

query = product.insert().values

@toidi hi! not exactly on the subject of the question) but what will asynchronous query look like?

You can't use canonical sqlalchemy. For async mode you can only use it to generate SQL, which later on you feed to databases.

It's well documented, please refer to Async SQL (Relational) Databases for details.

databases is not an ORM, check Tortoise ORM and Gino as well. Both have tutorials using FastAPI on their documentation. I'm on my phone, so I will not provide links to them. 馃憖

Since I could not find a solution using two linked tables, I changed my product model as follows

schema.py

class ProductSchema(BaseModel):
    name: str
    description: Optional[str] = None
    images: Optional[str] = None

models.py

class Product(Base):
    __tablename__ = 'products'

    id = Column(Integer, primary_key=True)
    name = Column(String, nullable=False)
    description = Column(String(50))
    # images = relationship('Image', back_populates='products')
    images = Column(String)


product = Product.__table__

routers.py

@router.post('/add_product/', status_code=201)
async def create_product(file: UploadFile = File(...),
                         name: str = Form(...),
                         description: str = Form(...)):

    _, ext = os.path.splitext(file.filename)
    IMG_DIR = os.path.join(BASEDIR, 'static/product_images')
    if not os.path.exists(IMG_DIR):
        os.makedirs(IMG_DIR)
    content = await file.read()
    if file.content_type not in ['image/jpeg', 'image/png']:
        raise HTTPException(status_code=406, detail="Only .jpeg or .png  files allowed")
    file_name = f'{uuid.uuid4().hex}{ext}'
    async with aiofiles.open(os.path.join(IMG_DIR, file_name), mode='wb') as f:
        await f.write(content)
    path_to_img = os.path.abspath(os.path.join(IMG_DIR, file_name))
    query = product.insert().values(name=name, description=description, images=path_to_img)
    item = await database.execute(query)

    response_object = {
        "id": item,
        "name": name,
        "description": description,
        "img": path_to_img
    }
    return response_object

And it works, brings back that response

{
  "id": 7,
  "name": "test_name",
  "description": "test_description",
  "img": "/home/jekson/Projects/img-text-reco/static/product_images/41607a4db8514cc6a6ec2e01120b5120.jpg"
}

But one question remains, the image field immediately returns the path to the loaded file relative to the hard disk (server). How do I pass the address to this field for internet access ? Something like

{
...
  "img": "http://mysite/static/product_images/41607a4db8514cc6a6ec2e01120b5120.jpg"
...
}

Thanks for the help everyone! :bow: :coffee:

@Lepiloff to have the image link relative to your server you will have to modify the data stored in the DB in your path operation code and add the http part, etc. That also means that then you cannot just return the SQLAlchemy models directly, you have to convert them to dicts or to Pydantic models by hand.

What I would suggest is: don't store the full path in DB. It depends on the exact file system currently deployed, so you can't migrate it easily. Store only the filename, like 41607a4db8514cc6a6ec2e01120b5120.jpg. And have the specific path as part of your code logic, independent of the DB. You could configure it with env vars: https://fastapi.tiangolo.com/advanced/settings/

And then in the frontend (I imagine you have a frontend of some type) also generate the full URL from the filename. That full URL will depend on where is the file actually served. You could also change that in the future, and just update it in a single point in the frontend configs, so that's another point for not storing the full path in DB.

And if you do that, then you can again just return the plain models from DB.

Was this page helpful?
0 / 5 - 0 ratings