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.

gkv
  • 360
  • 1
  • 8
  • 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. – Andrew Jul 13 '22 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" – gkv Jul 14 '22 at 03:08

3 Answers3

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.

rzlvmp
  • 7,512
  • 5
  • 16
  • 45
  • 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 – gkv Jul 14 '22 at 03:03
  • 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. – rzlvmp Jul 14 '22 at 04:49
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

gkv
  • 360
  • 1
  • 8
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
Hernán Alarcón
  • 3,494
  • 14
  • 16