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?
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.
Most helpful comment
databasesis 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. 馃憖