I'm trying to receive image file for my FastAPI file, upload it to a server and then Save url in database.
My create_product and create_category endpoint works as expected without the file: UploadFile = File(...) passed to the router function
I could successfully upload files with other routes and get their filename but not with the above mentioned routes
Routes:
#router.post('/category/create', response_model=schemas.CategoryOut)
def create_category(*, request: schemas.CategoryIn, file: UploadFile = File(...), db: Session = Depends(get_db), current_user: schemas.UserOut = Depends(get_current_active_user)):
category = storeview.get_category_by_slug(db, request.slug)
if category:
raise HTTPException(status.HTTP_400_BAD_REQUEST,
detail=f"Category with slug {request.slug} already exists!")
request.image = storeview.save_file(file)
return storeview.create_category(db, request)
#router.post('/product/create', response_model=schemas.Product)
async def create_product(*, file: UploadFile = File(...), request: schemas.ProductIn, category_id: int, db: Session = Depends(get_db), current_user: schemas.UserOut = Depends(get_current_active_user)):
product = storeview.get_category_by_slug(db, request.slug)
if product:
raise HTTPException(status.HTTP_400_BAD_REQUEST,
detail=f"Product with slug {request.slug} already exists!")
request.image = storeview.save_file(file)
return storeview.create_product(db, request, category_id)
Views:
def create_category(db: Session, request: schemas.CategoryIn):
new_category = models.Category(
name=request.name, slug=request.slug, image=request.image)
db.add(new_category)
db.commit()
db.refresh(new_category)
return new_category
def create_product(db: Session, request: schemas.ProductIn, category_id: int):
new_product = models.Product(**request.dict(), category_id=category_id)
db.add(new_product)
db.commit()
db.refresh(new_product)
return new_product
def save_file(file: UploadFile = File(...)):
result = cloudinary.uploader.upload(file.file)
url = result.get("url")
return url
Schemas:
class CategoryBase(BaseModel):
name: str
slug: str
image: str
class Category(CategoryBase):
id: int
class Config:
orm_mode = True
class CategoryIn(CategoryBase):
pass
class ProductBase(BaseModel):
name: str
description: Optional[str] = None
price: float
image: str
slug: str
class Product(ProductBase):
id: int
category_id: int
category: Category
class Config:
orm_mode = True
class ProductIn(ProductBase):
pass
class CategoryOut(CategoryBase):
products: List[Product] = []
id: int
class Config:
orm_mode = True
Related
When I send a multi-part form data it returns the error: "Unprocessable Entity"
the api endpoint:
#router.post('/', response_model=schemas.Category)
def add_category(
file: UploadFile,
category: schemas.CategoryCreate = Depends(schemas.CategoryCreate.as_form),
db: Session = Depends(get_db)
):
print(category)
print(file.filename)
The BaseModel classes:
class ProductCreate(BaseModel):
title: str
description: str
price: float
time: float
photo_url: str
#as_form
class CategoryCreate(BaseModel):
name: str
products: List[ProductCreate] = []
the as_form function:
def as_form(cls: Type[BaseModel]):
new_parameters = []
for field_name, model_field in cls.__fields__.items():
model_field: ModelField # type: ignore
new_parameters.append(
inspect.Parameter(
model_field.alias,
inspect.Parameter.POSITIONAL_ONLY,
default=Form(...) if not model_field.required else Form(model_field.default),
annotation=model_field.outer_type_,
)
)
async def as_form_func(**data):
return cls(**data)
sig = inspect.signature(as_form_func)
sig = sig.replace(parameters=new_parameters)
as_form_func.__signature__ = sig # type: ignore
setattr(cls, 'as_form', as_form_func)
return cls
note :
When I remove the field:
products: List[ProductCreate] = []
from CategoryCreate BaseModel, it works fine!
Thank you in advance
I am trying to submit data from HTML forms and validate it with a Pydantic model.
Using this code
from fastapi import FastAPI, Form
from pydantic import BaseModel
from starlette.responses import HTMLResponse
app = FastAPI()
#app.get("/form", response_class=HTMLResponse)
def form_get():
return '''<form method="post">
<input type="text" name="no" value="1"/>
<input type="text" name="nm" value="abcd"/>
<input type="submit"/>
</form>'''
class SimpleModel(BaseModel):
no: int
nm: str = ""
#app.post("/form", response_model=SimpleModel)
def form_post(form_data: SimpleModel = Form(...)):
return form_data
However, I get the HTTP error: "422 Unprocessable Entity"
{
"detail": [
{
"loc": [
"body",
"form_data"
],
"msg": "field required",
"type": "value_error.missing"
}
]
}
The equivalent curl command (generated by Firefox) is
curl 'http://localhost:8001/form' -H 'Content-Type: application/x-www-form-urlencoded' --data 'no=1&nm=abcd'
Here the request body contains no=1&nm=abcd.
What am I doing wrong?
I found a solution that can help us to use Pydantic with FastAPI forms :)
My code:
class AnyForm(BaseModel):
any_param: str
any_other_param: int = 1
#classmethod
def as_form(
cls,
any_param: str = Form(...),
any_other_param: int = Form(1)
) -> AnyForm:
return cls(any_param=any_param, any_other_param=any_other_param)
#router.post('')
async def any_view(form_data: AnyForm = Depends(AnyForm.as_form)):
...
It's shown in the Swagger as a usual form.
It can be more generic as a decorator:
import inspect
from typing import Type
from fastapi import Form
from pydantic import BaseModel
from pydantic.fields import ModelField
def as_form(cls: Type[BaseModel]):
new_parameters = []
for field_name, model_field in cls.__fields__.items():
model_field: ModelField # type: ignore
new_parameters.append(
inspect.Parameter(
model_field.alias,
inspect.Parameter.POSITIONAL_ONLY,
default=Form(...) if model_field.required else Form(model_field.default),
annotation=model_field.outer_type_,
)
)
async def as_form_func(**data):
return cls(**data)
sig = inspect.signature(as_form_func)
sig = sig.replace(parameters=new_parameters)
as_form_func.__signature__ = sig # type: ignore
setattr(cls, 'as_form', as_form_func)
return cls
And the usage looks like
#as_form
class Test(BaseModel):
param: str
a: int = 1
b: str = '2342'
c: bool = False
d: Optional[float] = None
#router.post('/me', response_model=Test)
async def me(request: Request, form: Test = Depends(Test.as_form)):
return form
you can use data-form like below:
#app.post("/form", response_model=SimpleModel)
def form_post(no: int = Form(...),nm: str = Form(...)):
return SimpleModel(no=no,nm=nm)
I implemented the solution found here Mause solution and it seemed to work
from fastapi.testclient import TestClient
from fastapi import FastAPI, Depends, Form
from pydantic import BaseModel
app = FastAPI()
def form_body(cls):
cls.__signature__ = cls.__signature__.replace(
parameters=[
arg.replace(default=Form(...))
for arg in cls.__signature__.parameters.values()
]
)
return cls
#form_body
class Item(BaseModel):
name: str
another: str
#app.post('/test', response_model=Item)
def endpoint(item: Item = Depends(Item)):
return item
tc = TestClient(app)
r = tc.post('/test', data={'name': 'name', 'another': 'another'})
assert r.status_code == 200
assert r.json() == {'name': 'name', 'another': 'another'}
You can do this even simpler using dataclasses
from dataclasses import dataclass
from fastapi import FastAPI, Form, Depends
from starlette.responses import HTMLResponse
app = FastAPI()
#app.get("/form", response_class=HTMLResponse)
def form_get():
return '''<form method="post">
<input type="text" name="no" value="1"/>
<input type="text" name="nm" value="abcd"/>
<input type="submit"/>
</form>'''
#dataclass
class SimpleModel:
no: int = Form(...)
nm: str = Form(...)
#app.post("/form")
def form_post(form_data: SimpleModel = Depends()):
return form_data
If you're only looking at abstracting the form data into a class you can do it with a plain class
from fastapi import Form, Depends
class AnyForm:
def __init__(self, any_param: str = Form(...), any_other_param: int = Form(1)):
self.any_param = any_param
self.any_other_param = any_other_param
def __str__(self):
return "AnyForm " + str(self.__dict__)
#app.post('/me')
async def me(form: AnyForm = Depends()):
print(form)
return form
And it can also be turned into a Pydantic Model
from uuid import UUID, uuid4
from fastapi import Form, Depends
from pydantic import BaseModel
class AnyForm(BaseModel):
id: UUID
any_param: str
any_other_param: int
def __init__(self, any_param: str = Form(...), any_other_param: int = Form(1)):
id = uuid4()
super().__init__(id, any_param, any_other_param)
#app.post('/me')
async def me(form: AnyForm = Depends()):
print(form)
return form
Create the class this way:
from fastapi import Form
class SomeForm:
def __init__(
self,
username: str = Form(...),
password: str = Form(...),
authentication_code: str = Form(...)
):
self.username = username
self.password = password
self.authentication_code = authentication_code
#app.post("/login", tags=['Auth & Users'])
async def auth(
user: SomeForm = Depends()
):
# return something / set cookie
Result:
If you want then to make an http request from javascript you must use FormData to construct the request:
const fd = new FormData()
fd.append('username', username)
fd.append('password', password)
axios.post(`/login`, fd)
Tldr: a mypy compliant, inheritable version of other solutions that produces the correct generated OpenAPI schema field types rather than any/unknown types.
Existing solutions set the FastAPI params to typing.Any to prevent the validation from occurring twice and failing, this causes the generated API spec to have any/unknown param types for these form fields.
This solution temporarily injects the correct annotations to the routes before schema generation, and resets them in line with other solutions afterwards.
# Example usage
class ExampleForm(FormBaseModel):
name: str
age: int
#api.post("/test")
async def endpoint(form: ExampleForm = Depends(ExampleForm.as_form)):
return form.dict()
form_utils.py
import inspect
from pydantic import BaseModel, ValidationError
from fastapi import Form
from fastapi.exceptions import RequestValidationError
class FormBaseModel(BaseModel):
def __init_subclass__(cls, *args, **kwargs):
field_default = Form(...)
new_params = []
schema_params = []
for field in cls.__fields__.values():
new_params.append(
inspect.Parameter(
field.alias,
inspect.Parameter.POSITIONAL_ONLY,
default=Form(field.default) if not field.required else field_default,
annotation=inspect.Parameter.empty,
)
)
schema_params.append(
inspect.Parameter(
field.alias,
inspect.Parameter.POSITIONAL_ONLY,
default=Form(field.default) if not field.required else field_default,
annotation=field.annotation,
)
)
async def _as_form(**data):
try:
return cls(**data)
except ValidationError as e:
raise RequestValidationError(e.raw_errors)
async def _schema_mocked_call(**data):
"""
A fake version which is given the actual annotations, rather than typing.Any,
this version is used to generate the API schema, then the routes revert back to the original afterwards.
"""
pass
_as_form.__signature__ = inspect.signature(_as_form).replace(parameters=new_params) # type: ignore
setattr(cls, "as_form", _as_form)
_schema_mocked_call.__signature__ = inspect.signature(_schema_mocked_call).replace(parameters=schema_params) # type: ignore
# Set the schema patch func as an attr on the _as_form func so it can be accessed later from the route itself:
setattr(_as_form, "_schema_mocked_call", _schema_mocked_call)
#staticmethod
def as_form(parameters=[]) -> "FormBaseModel":
raise NotImplementedError
# asgi.py
from fastapi.routing import APIRoute
from fastapi import FastAPI
from fastapi.openapi.utils import get_openapi
from fastapi.dependencies.utils import get_dependant, get_body_field
api = FastAPI()
def custom_openapi():
if api.openapi_schema:
return api.openapi_schema
def create_reset_callback(route, deps, body_field):
def reset_callback():
route.dependant.dependencies = deps
route.body_field = body_field
return reset_callback
# The functions to call after schema generation to reset the routes to their original state:
reset_callbacks = []
for route in api.routes:
if isinstance(route, APIRoute):
orig_dependencies = list(route.dependant.dependencies)
orig_body_field = route.body_field
is_modified = False
for dep_index, dependency in enumerate(route.dependant.dependencies):
# If it's a form dependency, set the annotations to their true values:
if dependency.call.__name__ == "_as_form": # type: ignore
is_modified = True
route.dependant.dependencies[dep_index] = get_dependant(
path=dependency.path if dependency.path else route.path,
# This mocked func was set as an attribute on the original, correct function,
# replace it here temporarily:
call=dependency.call._schema_mocked_call, # type: ignore
name=dependency.name,
security_scopes=dependency.security_scopes,
use_cache=False, # Overriding, so don't want cached actual version.
)
if is_modified:
route.body_field = get_body_field(dependant=route.dependant, name=route.unique_id)
reset_callbacks.append(
create_reset_callback(route, orig_dependencies, orig_body_field)
)
openapi_schema = get_openapi(
title="foo",
version="bar",
routes=api.routes,
)
for callback in reset_callbacks:
callback()
api.openapi_schema = openapi_schema
return api.openapi_schema
api.openapi = custom_openapi # type: ignore[assignment]
In the code, if it is done using (#app.post('/...')), it runs without any problems, but if it is done using (#router.post('/...')), it gives the following error;
TypeError: Cannot instantiate typing.Union
code:
router = APIRouter()
#app.get("/api/accounts/userslist/{domain}",
response_description="List all users")
async def account_list(
domain: str,
credentials: HTTPAuthorizationCredentials = Security(security)):
token = credentials.credentials
if (auth_handler.decode_token(token)):
domain = domains_db.find_one({'domain': domain})
if domain == None:
return JSONResponse(
status_code=status.HTTP_404_NOT_FOUND,
content='There is no user matching the requested domain.')
else:
accounts = users_db.find({'domain': domain['domain_id']})
list_account = [AuthModel(**account) for account in accounts]
return list_account
model:
class AuthModel(BaseModel):
id: PyObjectId = Field(default_factory=PyObjectId, alias="_id")
userid: UUID = Field(default_factory=uuid4)
firstname: Optional[str]
lastname: Optional[str]
domain: UUID = Field(default_factory=uuid4)
email: EmailStr
phone: Optional[str]
plain_secret: Optional[str]
status: Optional[str]
Are you including the router in the app in your main.py file as described here: https://fastapi.tiangolo.com/tutorial/bigger-applications/#include-the-apirouters-for-users-and-items ?
How can I turn this sync sqlalchemy query logic to async sqlalchemy.
def update_job_by_id(id:int, job: JobCreate,db: Session,owner_id):
existing_job = db.query(Job).filter(Job.id == id)
if not existing_job.first():
return 0
job.__dict__.update(owner_id=owner_id) #update dictionary with new key value of owner_id
existing_job.update(job.__dict__)
db.commit()
return 1
I tried to convert this logic into async sqlalchemy but no luck.Here is the async version of the above code:
async def update_job_by_id(self, id: int, job: PydanticValidationModel, owner_id):
job_query = await self.db_session.get(db_table, id)
if not job_query:
return 0
updated_job = update(db_table).where(db_table.id == id).values(job.__dict__.update(owner_id = owner_id)).execution_options(synchronize_session="fetch")
await self.db_session.execute(updated_job)
return 1
it produces this error:
AttributeError: 'NoneType' object has no attribute 'items'
Scope:
Making a simple Job Board API using FastAPI and Async SQLAlchemy and I am struggling through update feature of the API in function async def update_job_by_id which I mention above first checking the ID of the job if ID is True then it will export the PydanticModel to dict object to update the PydanticModel with owner_id and finally updating the Model and update the job, but unfortunately when I try to update the PydanticModel update(Job).values(**job_dict.update({"owner_id": owner_id})).where(Job.id == id).execution_options(synchronize_session="fetch") I get None instead of updated dict.
models/jobs.py
from sqlalchemy import Column, Integer, String, Boolean, Date, ForeignKey
from sqlalchemy.orm import relationship
from db.base_class import Base
class Job(Base):
id = Column(Integer, primary_key=True, index=True)
title = Column(String, nullable=False)
company_name = Column(String, nullable=False)
company_url = Column(String)
location = Column(String, nullable=False)
description = Column(String)
date_posted = Column(Date)
is_active = Column(Boolean, default=True)
owner_id = Column(Integer, ForeignKey('user.id'))
owner = relationship("User", back_populates="jobs")
schemas/jobs.py
from typing import Optional
from pydantic import BaseModel
from datetime import date, datetime
class JobBase(BaseModel):
title: Optional[str] = None
company_name: Optional[str] = None
company_url: Optional[str] = None
location: Optional[str] = "remote"
description: Optional[str] = None
date_posted: Optional[date] = datetime.now().date()
class JobCreate(JobBase):
title: str
company_name: str
location: str
description: str
class ShowJob(JobBase):
title: str
company_name: str
company_url: Optional[str]
location: str
date_posted: date
description: str
class Config():
orm_mode = True
routes/route_jobs.py
from typing import List
from fastapi import APIRouter, HTTPException, status
from fastapi import Depends
from db.repository.job_board_dal import job_board
from schemas.jobs import JobCreate, ShowJob
from db.repository.job_board_dal import Job
from depends import get_db
router = APIRouter()
#router.post("/create-job",response_model=ShowJob)
async def create_user(Job: JobCreate, jobs: Job = Depends(get_db)):
owner_id = 1
return await jobs.create_new_job(Job, owner_id)
#router.get("/get/{id}", response_model=ShowJob)
async def retrieve_job_by_id(id:int, id_job: job_board = Depends(get_db)):
job_id = await job_board.retrieve_job(id_job, id=id)
if not job_id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail=f"Job with id {id} does not exist")
return job_id
#router.get("/all", response_model=List[ShowJob])
async def retrieve_all_jobs(all_jobs: job_board = Depends(get_db)):
return await all_jobs.get_all_jobs()
#router.put("/update/{id}")
async def update_job(id: int, job: JobCreate, job_update: job_board = Depends(get_db)):
current_user = 1
response = await job_update.update_job_by_id(id = id, job = job, owner_id = current_user)
if not response:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail=f"Job with id {id} does not exist")
return {"response": "Successfully updated the Job."}
db/repository/job_board_dal.py
from typing import List
from sqlalchemy import update
from sqlalchemy.engine import result
from sqlalchemy.orm import Session
from sqlalchemy.future import select
from schemas.users import UserCreate
from schemas.jobs import JobCreate
from db.models.users import User
from db.models.jobs import Job
from core.hashing import Hasher
class job_board():
def __init__(self, db_session: Session):
self.db_session = db_session
async def register_user(self, user: UserCreate):
new_user = User(username=user.username,
email=user.email,
hashed_password=Hasher.get_password_hash(user.password),
is_active = False,
is_superuser=False
)
self.db_session.add(new_user)
await self.db_session.flush()
return new_user
async def create_new_job(self, job: JobCreate, owner_id: int):
new_job = Job(**job.dict(), owner_id = owner_id)
self.db_session.add(new_job)
await self.db_session.flush()
return new_job
async def retrieve_job(self, id:int):
item = await self.db_session.get(Job, id)
return item
async def get_all_jobs(self) -> List[Job]:
query = await self.db_session.execute(select(Job).order_by(Job.is_active == True))
return query.scalars().all()
async def update_job_by_id(self, id: int, job: JobCreate, owner_id):
_job = await self.db_session.execute(select(Job).where(Job.id==id))
(result, ) = _job.one()
job_dict = job.dict()
print(job_dict.update({"owner_id": owner_id}))
#print(job_update)
if not result:
return 0
job_update = update(Job).values(job_dict.update({"owner_id": owner_id})).where(Job.id == id).execution_options(synchronize_session="fetch")
return await self.db_session.execute(job_update)
If anyone can point out what am I missing here it would be much appreciated.
Sqlalchemy didn't support async operations until version 1.4, see this.
I'm trying to build a simple job board using Python, FastAPI and Async sqlalchemy by following the official FastAPI documentation. I'm having some issues regarding retrieving the data from database by ID.
The Following is hopefully a minimum reproducible code segment:
schemas/jobs.py
from typing import Optional
from pydantic import BaseModel
from datetime import date, datetime
class JobBase(BaseModel):
title: Optional[str] = None
company_name: Optional[str] = None
company_url: Optional[str] = None
location: Optional[str] = "remote"
description: Optional[str] = None
date_posted: Optional[date] = datetime.now().date()
class JobCreate(JobBase):
title: str
company_name: str
location: str
description: str
class ShowJob(JobBase):
title: str
company_name: str
company_url: Optional[str]
location: str
date_posted: date
description: str
class Config():
orm_mode = True
routes/route_jobs.py
from fastapi import APIRouter, HTTPException, status
from fastapi import Depends
from db.repository.job_board_dal import job_board
from schemas.jobs import JobCreate, ShowJob
from db.repository.job_board_dal import Job
from depends import get_db
router = APIRouter()
#router.post("/create-job",response_model=ShowJob)
async def create_user(Job: JobCreate, jobs: Job = Depends(get_db)):
owner_id = 1
return await jobs.create_new_job(Job, owner_id)
#router.get("/get/{id}")
def retrieve_job_by_id(id:int, job_board = Depends(get_db)):
#print(type(session))
job_id = job_board.retrieve_job(job_board, id=id)
if not job_id:
HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail=f"Job with id {id} does not exist")
return job_id
db/repository/job_board_dal.py
from sqlalchemy.orm import Session, query
from schemas.users import UserCreate
from schemas.jobs import JobCreate
from db.models.users import User
from db.models.jobs import Job
from core.hashing import Hasher
class job_board():
def __init__(self, db_session: Session):
self.db_session = db_session
async def register_user(self, user: UserCreate):
new_user = User(username=user.username,
email=user.email,
hashed_password=Hasher.get_password_hash(user.password),
is_active = False,
is_superuser=False
)
self.db_session.add(new_user)
await self.db_session.flush()
return new_user
async def create_new_job(self, job: JobCreate, owner_id: int):
new_job = Job(**job.dict(), owner_id = owner_id)
self.db_session.add(new_job)
await self.db_session.flush()
return new_job
def retrieve_job(self, id:int):
item = self.db_session.query(Job).filter(Job.id == id).first()
return item
depends.py
from db.session import async_session
from db.repository.job_board_dal import job_board
async def get_db():
async with async_session() as session:
async with session.begin():
yield job_board(session)
I tried to change the dependency
#router.get("/get/{id}")
def retrieve_job_by_id(id:int, id_job: job_board = Depends(get_db)):
#print(type(session))
job_id = job_board.retrieve_job(id_job, id=id)
if not job_id:
HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail=f"Job with id {id} does not exist")
return job_id
it gives me this error AttributeError: 'AsyncSession' object has no attribute 'query'.
Any help would be much appreciated.
AsyncSession class doesn't declare a query property.
You can use AsyncSession.get the fetch the Job record matching a given id. This method return None if no record is found matching the said id.
For example,
async def retrieve_job(self, id:int):
item = await self.db_session.get(Job, id)
return item