I want to get json and validate it. I can't just use pedantic #validator because additional validation requires a database connection or other I/O. How should I use all these checks correctly?
This is something that I want (just enumerate all dependencies for Body param)
from __future__ import annotations
from fastapi import FastAPI, Body, Depends
from fastapi.exceptions import RequestValidationError
from pydantic import BaseModel
app = FastAPI()
class ImportModel(BaseModel):
id: int
text: str | None
def f1(req: ImportModel = Body()):
if extra_check1(req):
return req
raise RequestValidationError("f1")
def f2(req: ImportModel = Body()):
if extra_check2(req):
return req
raise RequestValidationError("f2")
def f3(req: ImportModel = Body()):
if extra_check3(req):
return req
raise RequestValidationError("f3")
#etc...
#app.post('/import')
def import_smth(req: ImportModel = Depends(f1, f2, f3)):
return req
If all you need to do is perform checks without returning anything (new), you can just use the dependencies parameter of your path operation decorator:
#app.post("/import", dependencies=[Depends(f1), Depends(f2), Depends(f3)])
def import_smth(req: ImportModel):
return req
Related
I'm using fastapi-azure-auth to make call to my API impossible, if the user is not logged in (doesn't pass a valid token in the API call from the UI to be precise).
My question doesn't have anything to do with this particular library, it's about FastAPI in general.
I use a class (SingleTenantAzureAuthorizationCodeBearer) which is callable. It is used in two cases:
api.onevent("startup") - to connect to Azure
as a dependency in routes that user wants to have authentication in
To initialize it, it requires some things like Azure IDs etc. I provide those via a config file.
The problem is, this class is created when the modules get evaluated, so the values from the config file would have to be already present.
So, I have this:
dependecies.py
azure_scheme = SingleTenantAzureAuthorizationCodeBearer(
app_client_id=settings.APP_CLIENT_ID,
tenant_id=settings.TENANT_ID,
scopes={
f'api://{settings.APP_CLIENT_ID}/user_impersonation': 'user_impersonation',
}
)
api.py
from .dependencies import azure_scheme
api = FastAPI(
title="foo"
)
def init_api() -> FastAPI:
# I want to read configuration here
api.swagger_ui.init_oauth = {"clientID": config.CLIENT_ID}
return api
#api.on_event('startup')
async def load_config() -> None:
"""
Load OpenID config on startup.
"""
await azure_scheme.openid_config.load_config()
#api.get("/", dependencies=[Depends(azure_scheme)])
def test():
return {"hello": "world"}
Then I'd run the app with gunicorn -k uvicorn.workers.UvicornWorker foo:init_api().
So, for example, the Depends part will get evaluated before init_api, before reading the config. I would have to read the config file before that happens. And I don't want to do that, I'd like to control when the config reading happens (that's why I have init_api function where I initialize the logging and other stuff).
My question would be: is there a way to first read the config then initialize a dependency like SingleTenantAzureAuthorizationCodeBearer so I can use the values from config for this initialization?
#Edit
api.py:
from fastapi import Depends, FastAPI, Response
from fastapi.middleware.cors import CORSMiddleware
from .config import get_config
from .dependencies import get_azure_scheme
api = FastAPI(
title="Foo",
swagger_ui_oauth2_redirect_url="/oauth2-redirect",
)
api.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
def init_api() -> FastAPI:
api.swagger_ui_init_oauth = {
"usePkceWithAuthorizationCodeGrant": True,
"clientId": get_config().client_id,
}
return api
#api.get("/test", dependencies=[Depends(get_azure_scheme)])
def test():
return Response(status_code=200)
config.py:
import os
from functools import lru_cache
import toml
from pydantic import BaseSettings
class Settings(BaseSettings):
client_id: str
tenant_id: str
#lru_cache
def get_config():
with open(os.getenv("CONFIG_PATH", ""), mode="r") as config_file:
config_data = toml.load(config_file)
return Settings(
client_id=config_data["azure"]["CLIENT_ID"], tenant_id=config_data["azure"]["TENANT_ID"]
)
dependencies.py:
from fastapi import Depends
from fastapi_azure_auth import SingleTenantAzureAuthorizationCodeBearer
from .config import Settings, get_config
def get_azure_scheme(config: Settings = Depends(get_config)):
return SingleTenantAzureAuthorizationCodeBearer(
app_client_id=config.client_id,
tenant_id=config.tenant_id,
scopes={
f"api://{config.client_id}/user": "user",
},
)
How to create a custom connector in rasa for Viber connectivity
• This is my current custom connector file. the file name is viber.py and I am using rasa 2.8 ( I had to hide the hostname in webhook)
from http.client import HTTPResponse
from viberbot import Api
from viberbot.api.bot_configuration import BotConfiguration
from typing import Text, List, Dict, Any, Optional, Callable, Iterable, Awaitable
from asyncio import Queue
from sanic.request import Request
from rasa.core.channels import InputChannel
from rasa.core.agent import Agent
from rasa.core.channels.channel import UserMessage, CollectingOutputChannel, QueueOutputChannel
from rasa.utils.endpoints import EndpointConfig
from rasa import utils
from flask import Blueprint, request, jsonify
from sanic import Blueprint, response
from rasa.model import get_model, get_model_subdirectories
import inspect
from rasa.core.run import configure_app
bot_configuration = BotConfiguration(
name='Rasa_demo_one',
avatar='',
auth_token='4f831f01ef34ad38-9d057b2fd4ba804-8dd0cf1cdf5e39dc'
)
viber = Api(bot_configuration)
viber.set_webhook('<host doamin>/webhooks/viber/webhook')
class ViberInput(InputChannel):
"""Viber input channel implementation. Based on the HTTPInputChannel."""
#classmethod
def name(cls) -> Text:
return "viber"
#classmethod
def from_credentials(cls, credentials: Optional[Dict[Text, Any]]) -> InputChannel:
if not credentials:
cls.raise_missing_credentials_exception()
return cls(
credentials.get("viber_name"),
credentials.get("avatar"),
credentials.get("auth_token")
)
def __init__(self, viber_name: Text, avatar: Text, auth_token: Text) -> None:
"""Create a Viber input channel.
"""
self.viber_name = viber_name
self.avatar = avatar
self.auth_token = auth_token
self.viber = Api(
BotConfiguration(
name=self.viber_name,
avatar=self.avatar,
auth_token=self.auth_token
)
)
def blueprint(self, on_new_message: Callable[[UserMessage], Awaitable[Any]]) -> Blueprint:
viber_webhook = Blueprint("viber_webhook", __name__)
#viber_webhook.route("/", methods=["POST"])
async def incoming(request: Request) -> HTTPResponse:
viber_request = self.viber.parse_request(request.get_data())
if isinstance(viber_request):
message = viber_request.message
# lets echo back
self.viber.send_messages(viber_request.sender.id, [
message
])
return response.text("success")
return viber_webhook
• I created this file where the credentials file is located.
• This is the credential I put in the credentials.yml file
viber.ViberInput:
viber_name: "Rasa_demo_one"
avatar: ""
auth_token: "4f831f01ef34ad38-9d057b2fd4ba804-8dd0cf1cdf5e39dc"
• when I tries to run rasa with this configuration I got an error that says
RasaException: Failed to find input channel class for 'Viber.ViberInput'. Unknown input channel. Check your credentials configuration to make sure the mentioned channel is not misspelled. If you are creating your own channel, make sure it is a proper name of a class in a module.
You didn't tell about path of you file and filename itself..
If it's your correct credentials when your file must be located in rasa's root directory with viber.py name.
And looks like you forgot to import InputChannel
from rasa.core.channels.channel import InputChannel, UserMessage, CollectingOutputChannel, QueueOutputChannel
I am trying to add routes from a file and I don't know the actual arguments beforehand so I need to have a general function that handles arguments via **kwargs.
To add routes I am using add_api_route as below:
from fastapi import APIRouter
my_router = APIRouter()
def foo(xyz):
return {"Result": xyz}
my_router.add_api_route('/foo/{xyz}', endpoint=foo)
Above works fine.
However enrty path parameters are not fixed and I need to read them from a file, to achieve this, I am trying something like this:
from fastapi import APIRouter
my_router = APIRouter()
def foo(**kwargs):
return {"Result": kwargs['xyz']}
read_from_file = '/foo/{xyz}' # Assume this is read from a file
my_router.add_api_route(read_from_file, endpoint=foo)
But it throws this error:
{"detail":[{"loc":["query","kwargs"],"msg":"field required","type":"value_error.missing"}]}
FastAPI tries to find actual argument xyz in foo signature which is not there.
Is there any way in FastAPI to achieve this? Or even any solution to accept a path like /foo/... whatever .../?
This will generate a function with a new signature (I assume every parameter is a string):
from fastapi import APIRouter
import re
import inspect
my_router = APIRouter()
def generate_function_signature(route_path: str):
args = {arg: str for arg in re.findall(r'\{(.*?)\}', route_path)}
def new_fn(**kwargs):
return {"Result": kwargs['xyz']}
params = [
inspect.Parameter(
param,
inspect.Parameter.POSITIONAL_OR_KEYWORD,
annotation=type_
) for param, type_ in args.items()
]
new_fn.__signature__ = inspect.Signature(params)
new_fn.__annotations__ = args
return new_fn
read_from_file = '/foo/{xyz}' # Assume this is read from a file
my_router.add_api_route(
read_from_file,
endpoint=generate_function_signature(read_from_file)
)
However I am sure there is a better way of doing whatever you are trying to do, but I would need to understand your problem first
As described here, you can use the path convertor, provided by Starlette, to capture arbitrary paths. Example:
from fastapi import APIRouter, FastAPI
app = FastAPI()
my_router = APIRouter()
def foo(rest_of_path: str):
return {"rest_of_path": rest_of_path}
route_path = '/foo/{rest_of_path:path}'
my_router.add_api_route(route_path, endpoint=foo)
app.include_router(my_router)
Input test:
http://127.0.0.1:8000/foo/https://placebear.com/cache/395-205.jpg
Output:
{"rest_of_path":"https://placebear.com/cache/395-205.jpg"}
I am trying to create an audio server where I can upload various audio files, I have a requirement that I can only create one endpoint for creating, I have come up with this but it does show the request form to input data.
class AudioType(str, Enum):
Song = "Song"
Podcast = "Podcast"
Audiobook = "Audiobook"
#app.post("/{audio_type}", status_code=status.HTTP_200_OK)
def audio(audio_type: AudioType):
if audio_type == AudioType.Song:
def create_song(request: schemas.Song, db: Session = Depends(database.get_db)):
new_song = models.Song(name=request.name, duration=request.duration, uploadTime=request.uploadTime)
db.add(new_song)
db.commit()
db.refresh(new_song)
return new_song
elif audio_type == AudioType.Podcast:
def create_podcast(request: schemas.Podcast, db: Session = Depends(database.get_db)):
new_podcast = models.Podcast(name=request.name, duration=request.duration, uploadTime=request.uploadTime, host=request.host)
db.add(new_podcast)
db.commit()
db.refresh(new_podcast)
return new_podcast
elif audio_type == AudioType.Audiobook:
def create_audiobook(request: schemas.Audiobook, db: Session = Depends(database.get_db)):
new_audiobook = models.Audiobook(titile=request.title, author=request.author, narrator=request.narrator, duration=request.duration, uploadTime=request.uploadTime)
db.add(new_audiobook)
db.commit()
db.refresh(new_audiobook)
return new_audiobook
Your method doesn't accept the request object but only the audio_type.
Also from what I understand from your code, you may have multiple request bodies (schemas as you refer to them)
There are 2 options to what you want:
You need to declare your endpoint as follows:
from typing import Union
#app.post("/{audio_type}", status_code=status.HTTP_200_OK)
def audio(
request: Union[schemas.Song, schemas.Podcast,
schemas.Audiobook], audio_type: AudioType
):
... Your method ...
But the auto swagger of fastapi will not provide a schema example and you will have to provide examples manually, (which may or may not be possible, I don't really know and haven't tried it :/)
OR you can have a schema that can accept everything as Optional and the audio_type parameter:
from typing import Optional
class EndpointSchema(BaseModel):
audio_type: AudioType
song: Optional[schemas.Song]
podcast: Optional[schemas.Podcast]
audiobook: Optional[schemas.Audiobook]
#app.post("/audio", status_code=status.HTTP_200_OK)
def audio(request_body: EndpointSchema):
if request_body.audio_type == AudioType.Song:
... Continue with your request processing ...
Finally, very important: You are declaring internal methods (create_song etc.) that you are not calling afterward, so your code will do nothing. You don't need to do that, use the code you want to create a song, podcast, or audiobook directly inside the if, elif ... blocks!
I have a function like this,
async def predict(static: str = Form(...), file: UploadFile = File(...)):
return something
I have two parameters here, static and file and static is a string, file is buffer of uploaded file.
Now, is there a way to assign multiple types to one parameter, i.e., I want to make file parameter take either upload file or just string
Use Union type annotation
from fastapi import FastAPI, UploadFile, Form, File
from typing import Union
app = FastAPI()
#app.post(path="/")
async def predict(
static: str = Form(...),
file: Union[UploadFile, str] = File(...)
):
return {
"received_static": static,
"received_file": file if isinstance(file, str) else file.filename
}
AFAIK, OpenAPI schema doesn't support this config since it doesn't support multiple types. So, better to define multiple parameters to handle things differently. Also, IMHO, it is the better way
from fastapi import FastAPI, UploadFile, Form, File
app = FastAPI()
#app.post(path="/")
async def predict(
static: str = Form(...),
file: UploadFile = File(None), # making optional
file_str: str = Form(None) # making optional
):
return {
"received_static": static,
"received_file": file.filename if file else None,
"received_file_str": file_str if file_str else ''
}