How to dynamically create FastAPI routes/handlers for a list of Pydantic models?
Asked Answered
R

1

2

I have a problem using FastAPI, trying to dynamically define routes. It seems that the final route/handler defined overrides all the previous ones (see image below)

Situation: I have a lot of Pydantic models as a list (imported from models.py: simplified in example), and would like to create a GET endpoint for each model.

Something like this (my app is more complex — there is a reason it is structured as it is — but I've simplified it down to this, in order to find the problem, which still occurs):

# models.py

class Thing(BaseNode):
    value: str

class Other(BaseNode):
    value: str

model_list = [Thing, Other]



# app.py
from models import model_list

app = FastApi()


# Iterate over the models list and define a get function
for model in models_list:

    @app.get(f"/{model.__name__.lower()}")
    def get() -> str:
         print(model.__name__)
         return f"Getting {model.__name__)

Calling GET Thing returns the response for GET Other

I'm obviously doing something wrong (or FastAPI references the handler function in some weird way... by module/function name(?) so the get() function is being re-defined for each iteration?) Any ideas, or better way to structure this? (I can't manually write out a new function for each model!)

Thanks!

UPDATE: Update:

If I define the get() function inside another function, it works:

for model in models_list:
     def get(model):
         def _get():
             print("Getting", model)
                
             return f"Getting {model.__name__}"
         return _get

     app.get(.get(f"/{model.__name__.lower()}", name=f"{model.__name__}.View")(get(model))

Rutger answered 5/3 at 19:27 Comment(2)
please add the missing imports so that this code can run. Currently it does not run.Paleobiology
Also my guess would be that in the first example you're defining a function with the same name multiple times, which might result in some weird behavior. In the second example you do the same thing, but before it gets overwritten you are storing it. Can't test without runnable code though, just a theory.Paleobiology
P
2

Most likely the issue lies in the fact that you are using the same model reference for every endpoint created, and hence, it is always assigned the last value given in the for loop.

You should rather use a helper function that returns an inner function, as demonstrated in Option 2 of this answer.

Example

from fastapi import FastAPI, APIRouter, Request
from pydantic import BaseModel


class One(BaseModel):
    value: str


class Two(BaseModel):
    value: str


app = FastAPI()
models = [One, Two]


def create_endpoint(m_name: str):
    async def endpoint(request: Request):
        print(request.url)
        return f"You called {m_name}"
    return endpoint


for m in models:    
    app.add_api_route(f"/{m.__name__.lower()}", create_endpoint(m.__name__), methods=["GET"])
Philosophical answered 6/3 at 9:17 Comment(2)
How would you do for POST/PATCH methods that would take data corresponding to each model as input. (the equivalent to def myroute_one(data: One, request: Request) ? And also, how would you define the response model for such endpoints ?Predesignate
@Predesignate Please have a look at the implementation of add_api_route() - it should be straightforward. For instance, see methods and response_class parameters.Philosophical

© 2022 - 2024 — McMap. All rights reserved.