FastAPI read configuration before specifying dependencies - python

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

Related

Disable openapi-generator from overriding the controllers implementation to default

I have a project created with openapi-generator and a python-flask application is generated using the open api. I am using following command to generate my app.
openapi-generator generate -i OpenAPI/api.yaml -g python-flask --skip-validate-spec
But this command override my already implemented controller implementations to the default. Is there a way to,
Skip the controllers that was not updated in this iteration and create only the new ones.
If not skip generation of controller python files
I have already used .openapi-generator-ignore and ignored the controller repository.
But i am looking for something better?
I'm working on a project here which could helps you for generating openapi easily without have to execute or config anything just run your server. Here it is flask-toolkits
the implementation is via your view function and its really flexible. Its inspired by FastAPI's openapi setup
from typing import Optional
from flask import Flask
from flask_toolkits import AutoSwagger, APIRouter, Body, Header, Query
from flask_toolkits.responses import JSONResponse
app = Flask(__name__)
auto_swagger = AutoSwagger()
app.register_blueprint(auto_swagger)
router = APIRouter("email", __name__, url_prefix="/email")
#router.post("/read", tags=["Email Router"])
def get_email(
token: int = Header(),
id: int = Body(),
name: Optional[str] = Body(None),
race: Optional[str] = Query(None)
):
return JSONResponse({"id":id, "name": name})
app.register_blueprint(email_router)
if __name__ == "__main__":
app.run()
go to http://localhost:5000/docs
and here you go

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

Add route to FastAPI with custom path parameters

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

FastAPI: Loading multiple environments within the same settings class

I've been struggling to achieve this for a while now and it seems that I can't find my way around this. I have the following main entry point for my FastAPI project:
from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware
from starlette.responses import RedirectResponse
from app.core.config import get_api_settings
from app.api.api import api_router
def get_app() -> FastAPI:
api_settings = get_api_settings()
server = FastAPI(**api_settings.fastapi_kwargs)
server.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
server.include_router(api_router, prefix="/api")
#server.get("/", include_in_schema=False)
def redirect_to_docs() -> RedirectResponse:
return RedirectResponse(api_settings.docs_url)
return server
app = get_app()
Nothing too fancy so far. As you can see, I'm importing get_api_settings which holds my entire service config and it looks like this:
from functools import lru_cache
from typing import Any, Dict
from pydantic import BaseSettings
class APISettings(BaseSettings):
"""This class enables the configuration of your FastAPI instance
through the use of environment variables.
Any of the instance attributes can be overridden upon instantiation by
either passing the desired value to the initializer, or by setting the
corresponding environment variable.
Attribute `xxx_yyy` corresponds to environment variable `API_XXX_YYY`.
So, for example, to override `api_prefix`, you would set the environment
variable `API_PREFIX`.
Note that assignments to variables are also validated, ensuring that
even if you make runtime-modifications to the config, they should have
the correct types.
"""
# fastapi.applications.FastAPI initializer kwargs
debug: bool = False
docs_url: str = "/docs"
openapi_prefix: str = ""
openapi_url: str = "/openapi.json"
redoc_url: str = "/redoc"
title: str = "Api Backend"
version: str = "0.1.0"
# Custom settings
disable_docs: bool = False
environment: str
#property
def fastapi_kwargs(self) -> Dict[str, Any]:
"""This returns a dictionary of the most commonly used keyword
arguments when initializing a FastAPI instance.
If `self.disable_docs` is True, the various docs-related arguments
are disabled, preventing spec from being published.
"""
fastapi_kwargs: Dict[str, Any] = {
"debug": self.debug,
"docs_url": self.docs_url,
"openapi_prefix": self.openapi_prefix,
"openapi_url": self.openapi_url,
"redoc_url": self.redoc_url,
"title": self.title,
"version": self.version
}
if self.disable_docs:
fastapi_kwargs.update({
"docs_url": None,
"openapi_url": None,
"redoc_url": None
})
return fastapi_kwargs
class Config:
case_sensitive = True
# env_file should be dynamic depending on the
# `environment` env variable
env_file = ""
env_prefix = ""
validate_assignment = True
#lru_cache()
def get_api_settings() -> APISettings:
"""This function returns a cached instance of the APISettings object.
Caching is used to prevent re-reading the environment every time the API
settings are used in an endpoint.
If you want to change an environment variable and reset the cache
(e.g., during testing), this can be done using the `lru_cache` instance
method `get_api_settings.cache_clear()`.
"""
return APISettings()
I'm trying to prepare this service for multiple environments:
dev
stage
prod
For each of the above, I have three different .env files as follow:
core/configs/dev.env
core/configs/stage.env
core/configs/prod.env
As an example, here is how a .env file looks like:
environment=dev
frontend_service_url=http://localhost:3000
What I can't get my head around is how to dynamically set the env_file = "" in my Config class based on the environment attribute in my APISettings BaseSettings class.
Reading through Pydantic's docs I thought I can use the customise_sources classmethod to do something like this:
def load_envpath_settings(settings: BaseSettings):
environment = # not sure how to access it here
for env in ("dev", "stage", "prod"):
if environment == env:
return f"app/configs/{environment}.env"
class APISettings(BaseSettings):
# ...
class Config:
case_sensitive = True
# env_file = "app/configs/dev.env"
env_prefix = ""
validate_assignment = True
#classmethod
def customise_sources(cls, init_settings, env_settings, file_secret_settings):
return (
init_settings,
load_envpath_settings,
env_settings,
file_secret_settings,
)
but I couldn't find a way to access the environment in my load_envpath_settings. Any idea how to solve this? Or if there's another way to do it? I've also tried creating another #property in my APISettings class which which would basically be the same as the load_envpath_settings but I couldn't refer it back in the Config class.
First; usually you'd copy the file you want to have active into the .env file, and then just load that. If you however want to let that .env file control which of the configurations that are active:
You can have two sets of configuration - one that loads the initial configuration (i.e. which environment is the active one) from .env, and one that loads the actual application settings from the core/configs/<environment>.env file.
class AppSettings(BaseSettings):
environment:str = 'development'
This would be affected by the configuration given in .env (which is the default file name). You'd then use this value to load the API configuration by using the _env_file parameter, which is support on all BaseSettings instances.
def get_app_settings() -> AppSettings:
return AppSettings()
def get_api_settings() -> APISettings:
app_settings = get_app_settings()
return APISettings(_env_file=f'core/configs/{app_settings.environment}.env') # or os.path.join() and friends

Categories