Add route to FastAPI with custom path parameters - python

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"}

Related

FastAPI read configuration before specifying dependencies

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",
},
)

Is it possible to create a chain of dependencies in fastapi?

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

How to create a custom connector in rasa for Viber connectivity

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

unable to call a defined function, invalid syntax on a line i have used before in the code

#app.get("/drogaraia")
def scraperaia(urlbase="https://www.drogaraia.com.br/medicamentos",maximodepaginas=10):
listaprincipal= []
pagina=2
contador=1
while pagina<maximodepaginas:
testeurl= ((urlbase)+".html?p="+str(pagina))
page = requests.get(testeurl)
results= BeautifulSoup(page.content,"html.parser")
remedios = results.find_all("div",class_="container")
for remedio in remedios:
try:
link=(remedio.find("a", class_="show-hover"))['href']
preco=remedio.find(class_="price").getText().strip()
titulo=(remedio.find("a", class_="show-hover")).getText()
categoria=urlbase.rsplit('/',1)[-1]
listaremedio=[{'link':link,'preco':preco,'titulo':titulo,'categoria':categoria}]
listaprincipal.extend(listaremedio)
except:
pass
contador=contador+1
pagina=pagina+1
return(listaprincipal)
#app.get("/drogaraia/medicamentos/monitores-e-testes/teste-de-controle-glicemicos")
scraperaia(urlbase="https://www.drogaraia.com.br/medicamentos/monitores-e-testes/teste-de-controle-glicemicos",maximodepaginas=10)
Error message here:
scraperaia(urlbase="https://www.drogaraia.com.br/medicamentos/monitores-e-testes/teste-de-controle-glicemicos",maximodepaginas=10)
^^^^^^^^^^
SyntaxError: invalid syntax
I don't see how it can be wrong syntax. I have tried not assigning the variables inside the scraperaia() function, like so:
urlbase="https://www.drogaraia.com.br/medicamentos/monitores-e-testes/teste-de-controle-glicemicos"
maximodepaginas=10
scraperaia(urlbase,maximodepaginas)
and it still doesnt work.
The last two lines of your provided code are wrong. You need to use def to define a function.
You can define multiple routes bound to the same function. If you would like to know which route was used, you can use the Request object to get the request URL path (see documentation here and here). Working example below:
from fastapi import FastAPI, Request
app = FastAPI()
#app.get("/store")
#app.get("/store/monitors-and-tests")
def main(request: Request, baseurl: str = "https://someurl", maxpages: int = 10):
return {"Called from": request.url.path}

How to make query parameter take two types of inputs in fastapi?

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 ''
}

Categories