1

I'm building a Fast API server that serves code on behalf of my customers.

So my directory structure is:

project
|  main.py
|--customer_code (mounted at runtime)
   |  blah.py

Within main.py I have:

from customer_code import blah
from fastapi import FastAPI

app = FastAPI()

...

@app.post("/do_something")
def bar(# I want this to have the same type as blah.foo()):
    blah.foo()

and within blah.py I have:

from pydantic import BaseModel

class User(BaseModel):
    id: int
    name = 'John Doe'

def foo(data : User):
    # does something

I don't know a priori what types my customers' code (in blah.py) will expect. But I'd like to use FastAPI's built-in generation of Open API schemas (rather than requiring my customers to accept and parse JSON inputs).

Is there a way to set the types of the arguments to bar to be the same as the types of the arguments to foo?

Seems like one way would be to do exec with fancy string interpolation but I worry that that's not really Pythonic and I'd also have to sanitize my users' inputs. So if there's another option, would love to learn.

2
  • Try inspecting the return type at runtime and setting that as the type annotation. It won't type check but should work. Or maybe you could customize the schema somehow rather than setting type annotations. Commented Jul 13, 2022 at 23:59
  • I think your first idea is to enforce type safety in my application code, with something like the inspect library. Not sure what you mean by "customize the schema somhow" Commented Jul 14, 2022 at 3:08

3 Answers 3

0

Does this answering your question?

def f1(a: str, b: int):
  print('F1:', a, b)

def f2(c: float):
  print('F2:', c)

def function_with_unknown_arguments_for_f1(*args, **kwargs):
  f1(*args, **kwargs)

def function_with_unknown_arguments_for_f2(*args, **kwargs):
  f2(*args, **kwargs)

def function_with_unknown_arguments_for_any_function(func_to_run, *args, **kwargs):
  func_to_run(*args, **kwargs)

function_with_unknown_arguments_for_f1("a", 1)
function_with_unknown_arguments_for_f2(1.1)
function_with_unknown_arguments_for_any_function(f1, "b", 2)
function_with_unknown_arguments_for_any_function(f2, 2.2)

Output:

F1: a 1
F2: 1.1
F1: b 2
F2: 2.2

Here is detailed explanation about args and kwargs

In other words post function should be like

@app.post("/do_something")
def bar(*args, **kwargs):
    blah.foo(*args, **kwargs)

To be able handle dynamically changing foos

About OpenAPI:

Pretty sure that it is possible to override documentation generator classes or functions and set payload type based on foo instead of bar for specific views.
Here is couple examples how to extend OpenAPI. It is not related to your question directly but may help to understand how it works at all.

Sign up to request clarification or add additional context in comments.

2 Comments

Yeah so if I run: curl -X 'POST' \ 'http://0.0.0.0:8000/do_something?args=&kwargs=name=hello&id=5' \ -H 'accept: application/json' \ -d '' I get the following: TypeError: foo() got an unexpected keyword argument 'args' because I'm passing in a literal argument called "args" (required by FastAPI) into foo
Yes, blah.foo need to handle request arguments by itself. If you trying to pass args named parameter foo definition should be like def foo(args: whatever). bar only passing arguments as is. That is only possible way satisfying bar function prototype provided in your question.
0

Ended up using this answer:

from fastapi import Depends, FastAPI
from inspect import signature
from pydantic import BaseModel, create_model


sig = signature(blah.foo)

query_params = {}
for k in sig.parameters:
  query_params[k] = (sig.parameters[k].annotation, ...)

query_model = create_model("Query", **query_params)

So then the function looks like:

@app.post("/do_something")
def bar(params: query_model = Depends()):
    p_as_dict = params.as_dict()
    return blah.foo(**p_as_dict)

I don't quite get what I want (there's no nice text box, just a JSON field with example inputs), but it's close: screenshot of Swagger UI

Comments

0

First consider the definition of a decorator. From Primer on Python Decorators:

a decorator is a function that takes another function and extends the behavior of the latter function without explicitly modifying it

app.post("/do_something") returns a decorator that receives the def bar(...) function in your example. About the usage of @, the same page mentioned before says:

@my_decorator is just an easier way of saying say_whee = my_decorator(say_whee)

So you could just use something like:

app.post("/do_something")(blah.foo)

And the foo function of blah will be exposed by FastAPI. It could be the end of it unless you also want perform some operations before foo is called or after foo returns. If that is the case you need to have your own decorator.

Full example:

# main.py
import functools
import importlib
from typing import Any

from fastapi import FastAPI

app = FastAPI()


# Create your own decorator if you need to intercept the request/response
def decorator(func):

    # Use functools.wraps() so that the returned function "look like"
    # the wrapped function
    @functools.wraps(func)
    def wrapper_decorator(*args: Any, **kwargs: Any) -> Any:
        # Do something before if needed
        print("Before")
        value = func(*args, **kwargs)
        # Do something after if needed
        print("After")
        return value

    return wrapper_decorator


# Import your customer's code
blah = importlib.import_module("customer_code.blah")

# Decorate it with your decorator and then pass it to FastAPI
app.post("/do_something")(decorator(blah.foo))
# customer_code.blah.py

from pydantic import BaseModel


class User(BaseModel):
    id: int
    name = 'John Doe'


def foo(data: User) -> User:
    return data

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.