How to access **kwargs in Pydantic's validator method - python

I wanted to validate the below stated payload -
payload = {
"emailId":['jeet#steinnlabs.com', 'jeet#dope.security'],
"role":"Administrator",
}
Hence, I created a class using Pydantic's BaseModel like this -
class SendInvitePayloadValidator(BaseModel):
emailId: List[str]
role: str
class Config:
extra = Extra.forbid
#validator('emailId')
def validate_email(cls, emailID):
pass
Now, I also wanted to validate each key of payload e.g emailId but in order to do that I need another an extra value lets say tenantId.
If I include tenantId in the model then the payload validation won't work.
So is there a way I could just pass few extra values to the validator method?

Related

getting joined tables from sqlmodel as a nested responde model in fastapi

I cannot figure it out how to display a one to many relationship using fastapi and sqlmodel. I've read through this question but my case seems to be slightly different. Specially in the function call.
This is my schemas.py:
from typing import Optional
from sqlmodel import Field, Relationship, SQLModel
class BinaryBase(SQLModel):
product_id: int
software_install_path: Optional[str] = None
host_id: int = Field(foreign_key="host.id", nullable=False)
class Binary(BinaryBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
host_id: int = Field(foreign_key="host.id", nullable=False)
host: "Host" = Relationship(back_populates="binaries")
class HostBase(SQLModel):
name: str
region: Optional[str] = None
os_version: Optional[str] = None
network: Optional[int] = None
class Host(HostBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
binaries: list[Binary] = Relationship(back_populates='host')
class HostReadWithBinary(HostBase):
bins: list[HostBase] = []
class BinaryReadWithHost(BinaryBase):
host: BinaryBase
And this is my main.py:
from fastapi import Depends, FastAPI
from sqlmodel import Session, col, select
...
#app.get(
"/binaries/",
response_model=list[HostReadWithBinary]
)
def get_binary(name: Optional[str] = None, session: Session = Depends(get_session)) -> list[HostReadWithBinary]:
query = select(Host).limit(100)
if name:
query = query.where(col(Host.name).contains(name.lower()))
return session.exec(query).all()
The Host table represents the 1 part and the Binary table represents the many part. I would like to get a response of all the BinaryBase attributes for all the hosts eagerly. But what I get is this:
[
{
"name": "hkl20014889",
"region": "HK",
"os_version": "Red Hat 6.10",
"network": 3,
"bins": []
},
{
"name": "hkl20016283",
"region": "HK",
"os_version": "Red Hat 6.10",
"network": 3,
"bins": []
},
....
Theoreticaly bins should hold the attributes of the Host table when id in Host joins host_id in Binary.
You need to realize that when you define a response_model for a route, it will always try to parse whatever data comes out of your route handler function (get_binary in this case) through that model. This is done by essentially calling the .from_orm method on the response model, which goes through all fields defined on it and tries to find corresponding attributes (i.e. with the same names) on the object you pass to it.
The model you specified (in a list, but the argument stands) is HostReadWithBinary. Aside from the fields defined on its parent model HostBase it only has the field bins, which is supposed to be a list of HostBase.
First of all, I think you meant to declare that bins field to be of the type list[BinaryBase], not list[HostBase]. If you had named the field correctly, this would have caused an error, but this is where your second mistake comes in.
You also named the field on your response model bins, but your handler function performs a query that returns a list of Host model instances. That model does not have a bins field. It has a field named binaries. This means that when the from_orm method gets to the HostReadWithBinary.bins field, it checks if the corresponding Host instance has an attribute named bins. It does not find one, but no problem because you set a default for HostReadWithBinary.bins, namely the empty list [], so that is what is set on each of the resulting response model instances.
You should therefore be able to fix your error by changing the response model definition like this:
class HostReadWithBinary(HostBase):
binaries: list[BinaryBase] = []
Alternatively, you can change the name of the relationship field on your Host model:
class Host(HostBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
bins: list[Binary] = Relationship(back_populates='host')
class HostReadWithBinary(HostBase):
bins: list[BinaryBase] = []
They need to be the same, otherwise parsing an object of one model to the other will not work (properly).
Side note: You also mistakenly annotated the host field on BinaryReadWithHost with BinaryBase instead of HostBase.
PS: I also just noticed a minor mistake related to type annotations. You declare the return type of your route handler function to be list[HostReadWithBinary], but that is not what it returns. It returns list[Host]. This is part of the misunderstanding with the response models. The decorated version of your route is what returns list[HostReadWithBinary]. Your route handler get_binary by itself (i.e. before decoration) returns list[Host], which is then passed to the wrapper around it and that wrapper parses it to list[HostReadWithBinary] and sends that data on its way (to the client eventually). This wrapper action obviously happens behind the scenes and is part of that FastAPI decorator magic.

Use FastAPI to parse incoming POST request from Slack

I'm building a FastAPI server to receive requests sent by slack slash command. Using the code below, I could see that the following:
token=BLAHBLAH&team_id=BLAHBLAH&team_domain=myteam&channel_id=BLAHBLAH&channel_name=testme&user_id=BLAH&user_name=myname&command=%2Fwhatever&text=test&api_app_id=BLAHBLAH&is_enterprise_install=false&response_url=https%3A%2F%2Fhooks.slack.com%2Fcommands%BLAHBLAH&trigger_id=BLAHBLAHBLAH
was printed, which is exactly the payload I saw in the official docs. I'm trying to use the payload information to do something, and I'm curious whether there's a great way of parsing this payload info. I can definitely parse this payload using split function or any other beautiful functions, but I'm curious whether there is a 'de facto' way of dealing with slack payload. Thanks in advance!
from fastapi import FastAPI, Request
app = FastAPI()
#app.post("/")
async def root(request: Request):
request_body = await request.body()
print(request_body)
Receive JSON data
You would normally use Pydantic models to declare a request body - if you were about to receive data in JSON form - thus benefiting from the validation that Pydantic has to offer (for more options on how to post JSON data, have a look at this answer). So, you would define a model like this:
from pydantic import BaseModel
class Item(BaseModel):
token: str
team_id: str
team_domain: str
# etc.
#app.post("/")
def root(item: Item):
print(item.dict()) # convert to dictionary (if required)
return item
The payload would look like this:
{
"token": "gIkuvaNzQIHg97ATvDxqgjtO"
"team_id": "Foo",
"team_domain": "bar",
# etc.
}
Receive Form data
If, however, you were about to receive the payload as Form data, just like what slack API does (as shown in the link you provided), you could use Form fileds. With Form fields, your payload will still be validated against those fields and the type you define them with. You would need, however, to define all the parameters in the endpoint, as described in the above link and as shown below:
from fastapi import Form
#app.post("/")
def root(token: str = Form(...), team_id: str = Form(...), team_domain: str = Form(...)):
return {"token": token, "team_id": team_id, "team_domain": team_domain}
or to avoid that, you may want to have a look at this post, which describes how to use Pydantic models with Form fields. As suggested in one of the answers, you can do that even without using Pydantic models, but instead with creating a custom dependency class using the #dataclass decorator, which allows you to define classes with less code. Example:
from dataclasses import dataclass
from fastapi import FastAPI, Form, Depends
#dataclass
class Item:
token: str = Form(...)
team_id: str = Form(...)
team_domain: str = Form(...)
#...
#app.post("/")
def root(data: Item = Depends()):
return data
As FastAPI is actually Starlette underneath, even if you still had to access the request body in the way you do in the question, you should rather use methods such as request.json() or request.form(), as described in Starlette documentation, which allow you to get the request body parsed as JSON or form-data, respectively.

return pydantic model with field names instead of alias as fastapi response

I am trying to return my model with the defined field names instead of its aliases.
class FooModel(BaseModel):
foo: str = Field(..., alias="bar")
#app.get("/") -> FooModel:
return FooModel(**{"bar": "baz"})
The response will be {"bar": "baz"} while I want {"foo": "baz"}. I know it's somewhat possible when using the dict method of the model, but it doesn't feel right and messes up the typing of the request handler.
#app.get("/") -> FooModel:
return FooModel(**{"bar": "baz"}).dict(by_alias=False)
I feel like it should be possible to set this in the config class, but I can't find the right option.
You can add response_model_by_alias=False to path operation decorator. This key is mentioned here in the documentation.
For example:
#app.get("/model", response_model=Model, response_model_by_alias=False)
def read_model():
return Model(alias="Foo")

FastAPI: Having a dependency through Depends() and a schema refer to the same root level key without ending up with multiple body parameters

I have a situation where I want to authorize the active user against one of the values (Organization) in a FastAPI route. When an object of a particular type is being submitted, one of the keys (organization_id) should be present and the user should be verified as having access to the organization.
I've solved this as a dependency in the API signature to avoid having to replicate this code across all routes that needs access to this property:
def get_organization_from_body(organization_id: str = Body(None),
user: User = Depends(get_authenticated_user),
organization_service: OrganizationService = Depends(get_organization_service),
) -> Organization:
if not organization_id:
raise HTTPException(status_code=400, detail='Missing organization_id.')
organization = organization_service.get_organization_for_user(organization_id, user)
if not organization:
raise HTTPException(status_code=403, detail='Organization authorization failed.')
return organization
This works fine, and if the API endpoint expects an organization through an organization_id key in the request, I can get the organization directly populated by introducing get_organization_from_body as a dependency in my route:
#router.post('', response_model=Bundle)
async def create_bundle([...]
organization: Organization = Depends(get_organization_from_body),
) -> Model:
.. and if the user doesn't have access to the organization, an 403 exception is raised.
However, I also want to include my actual object on the root level through a schema model. So my first attempt was to make a JSON request as:
{
'name': generated_name,
'organization_id': created_organization['id_key']
}
And then adding my incoming Pydantic model:
#router.post('', response_model=Bundle)
async def create_bundle(bundle: BundleCreate,
organization: Organization = Depends(get_organization_from_body),
[...]
) -> BundleModel:
[...]
return bundle
The result is the same whether the pydantic model / schema contains organization_id as a field or not:
class BundleBase(BaseModel):
name: str
class Config:
orm_mode = True
class BundleCreate(BundleBase):
organization_id: str
client_id: Optional[str]
.. but when I introduce my get_organization_from_body dependency, FastAPI sees that I have another dependency that refers to a Body field, and the description of the bundle object has to be moved inside a bundle key instead (so instead of "validating" the organization_id field, the JSON layout needs to change - and since I feel that organization_id is part of the bundle description, it should live there .. if possible).
The error message tells me that bundle was expected as a separate field:
{'detail': [{'loc': ['body', 'bundle'], 'msg': 'field required', 'type': 'value_error.missing'}]}
And rightly so, when I move name inside a bundle key instead:
{
'bundle': {
'name': generated_name,
},
'organization_id': created_organization['id_key']
}
.. my test passes and the request is successful.
This might be slightly bike shedding, but if there's a quick fix to work around this limitation in any way I'd be interested to find a way to both achieve validation (either through Depends() or in some alternative way without doing it explicitly in each API route function that requires that functionality) and a flat JSON layout that matches my output format closer.
Prior to FastAPI 0.53.2, dependencies for the body were resolved the way you are trying to do. Such code:
class Item(BaseModel):
val_1: int
val_2: int
def get_val_1(val_1: int = Body(..., embed=True)):
return val_1
#app.post("/item", response_model=Item)
def handler(full_body: Item, val_1: int = Depends(get_val_1)):
return full_body
Expected such body:
{
"val_1": 0,
"val_2": 0
}
But starting from version 0.53.2, different body dependencies are automatically embedded (embed=True) and the code above expects the following body:
{
"full_body": {
"val_1": 0,
"val_2": 0
},
"val_1": 0
}
Now, in order to have access to the model for the whole body and to have access to its elements as a separate dependency, you need to use same dependency for the body model everywhere:
def get_val_1(full_body: Item):
return full_body.val_1
#app.post("/item", response_model=Item)
def handler(full_body: Item, val_1: int = Depends(get_val_1)):
return full_body
Update for shared dependency
You can share one body dependency for multiple models, but in this case, two conditions must be met: the names of the dependencies must be the same and their types must be compatible (through inheritance or not). Example below:
class Base(BaseModel):
val_1: int
class NotBase(BaseModel):
val_1: int
class Item1(Base):
val_2: int
class Item2(Base):
val_3: int
def get_val1_base(full_body: Base):
return full_body.val_1
def get_val1_not_base(full_body: NotBase):
return full_body.val_1
#app.post("/item1", response_model=Item1)
def handler(full_body: Item1, val_1: int = Depends(get_val1_base)):
return full_body
#app.post("/item2", response_model=Item2)
def handler(full_body: Item2, val_1: int = Depends(get_val1_not_base)):
return full_body

How to validate JSON field with name "from"

I want to validate JSON object (it is in Telegram Bot API) which contains from field (which is reserved word in Python) by using pydantic validator. So my model should look like the following:
class Message(BaseModel):
message_id: int
from: Optional[str]
date: int
chat: Any
...
But using from keyword is not allowed in this context.
How could I do this?
Note: this is different than "Why we can't use keywords as attributes" because here we get external JSON we don't control and we anyway should handle JSON with from field.
I believe you can replace from with from_.
You can do it like this:
class Message(BaseModel):
message_id: int
from_: Optional[str]
date: int
chat: Any
class Config:
fields = {
'from_': 'from'
}
...
There might be a way to do this using a class statement, but I didn't see anything in a quick skim of the documentation. What you could do is use dynamic model creation instead.
fields = {
'message_id': (int,),
'from': (Optional[str], ...),
'date': (int, ...),
'chat': (Any, ...)
}
Message = create_model("Message", **fields)

Categories