fastapi+pydantic query parameter checking for complex arguments - fastapi

Based on this API definition, my api supports queries like:
GET http://my.api.url/posts?sort=["title","ASC"]&range=[0, 24]&filter={"q":"bar"}
where some of the checks needed are
sort[1] is either "asc" or "desc" (case should not matter)
filter has the key "q". filter can have other keys.
range is a list of two integers. range[0] is less then or equal to range[1]
In fastapi path definitions I currently define filter, sort, and range as strings as in the code below, convert them using json.loads, and do checks.
#r.get(
"/users",
response_model=List[User],
response_model_exclude_none=True,
)
async def list_users(
filter: Optional[str] = None,
sort: Optional[str] = None,
range: Optional[str] = None,
...
):
...
How can I use pydantic definitions for checks and API definition instead of just using str, such that checks are done by pydantic, and openapi schema definitions are more descriptive?

There are some ways to do what you want. But FastAPI only allow sequence structures (list, tuples, set, sequence) for Query and Header params, so that &filter={"q":"bar"} it won't work on FastAPI at least for 0.75.0 version where I am working.
If you wish to support a Dictionary you should use POST method with Body params. And then you can wrap your values around a Pydantic model to support validation.
Below there are two ways to implement this in FastAPI.
Solution 1:
class FilterModel(BaseModel):
filter: dict
#router.post(
"/posts"
)
async def list_users(
filters: FilterModel,
sort: Tuple[str, Literal["DESC", "ASC", "desc", "asc"]] = Query(("title", "ASC")),
ranges: Tuple[int, int] = Query((0, 24))
) -> None:
print(filters)
print(sort)
print(ranges)
model = MyModel(sort=sort, range=ranges)
print(model)
This will allow you to call api like:
POST http://my.api.url/posts?sort=title&sort=ASC&ranges=0&ranges=24
With body:
{
"filter"={"q":"bar"}
}
Solution 2:
class MyModel(BaseModel):
sortBy: Optional[str]
sortOrder: Optional[Literal["DESC", "ASC", "desc", "asc"]]
min_range: Optional[int]
max_range: Optional[int]
class FilterModel(BaseModel):
filter: dict
#router.post(
"/posts"
)
async def list_users(
filters: FilterModel,
model: MyModel = Depends()
) -> None:
print(filters)
print(model)
This will allow you to call api like:
POST http://my.api.url/posts?sortBy="title"&sortOrder="ASC"&min_range=0&max_range=24
With body:
{
"filter"={"q":"bar"}
}
If you wish to use GET method and the filter you have to do some hacky things.

Related

fastapi + sqlalchemy + pydantic → how to read data return to schema

I'm trying to use FastApi, sqlalchemy and pydantic.
I have in request body, a schema, a field type list, and optional named files (files: list[schemas.ImageBase]).
I need to read all the entered data one by one but it doesn't let me loop for the returned list.
This also happens to me when I get a query returned using for example:
def get_setting(svalue:int, s_name: str):
db = SessionLocal()
query = db.query(models.Setting)\
.filter(
models.Setting.svalue == svalue,
models.Setting.appuser == s_name
).all()
return query
in the -->
async def get_settings(svalue: int, name:str):
**values**=crud.get_setting(svalue=svalue,s_name=name)
return {"settings" : values}
But I can't loop (with the for) **values**
Why? I have to set something or I'm wrong using the query or pydantic?
I expect to be looking for a list or dictionary and being able to read the data

How do I specify a type for a function parameter that optionally includes a given method?

Updated Question
I want to define a function named bsearch() to do binary searches against arrays of arbitrary object types. When I invoke the function, I want it to check whether or not the Type of the array contains a compare() method and use it, if it does. If it does not, I want it to fall back to using < and === (so it will work with strings and numbers).
What should the function declaration look like? (I don't need an actual implementation, just the syntax for a type-safe solution.)
Or maybe I'm going about this all wrong? How can I create a function that uses a method built into a parameter type if it exists, or use some other function when it doesn't?
Original Question
This is the original question, but I've replaced it with the above as it seems this wasn't getting my point across.
I want to define a function named bsearch() to do binary searches against arrays of arbitrary object types. So I'd like to do something like this:
type Comparator = <Type>(a: Type, b: Type) => -1 | 0 | 1;
static bsearch<Type extends { compare?: Comparator }>(
ary: Type[],
value: Type
): number { ... }
My goal is to specify that Type must extend a type that may or may not include the compare method. In my function, I will check whether the compare method exists on the value parameter and call if it does, or use a generic function (that uses < and ===) if it does not.
The definition of bsearch() does not produce any warnings or errors, but attempts to invoke it from my unit test does:
class Person {
name: string;
length: number;
compare: Comparator<Person>; // What goes here?
}
describe('Utils tests', () => {
const arrayOfInt = [10, 20, 30, 40];
const arrayOfStr = ['Alfred', 'Bob', 'Chuck'];
const arrayOfPersons: Person = [
{name:'Barney',length:2},
{name:'Fred',length:6}
{name:'Wilma',length:12},
];
it('can find integer in an array of integers', () => {
let search_for = 30;
let result = Utils.bsearch(arrayOfInt, search_for)
expect(result).to.be.equal(2);
});
it('can find string in an array of strings', () => {
let search_for = 'Bob';
let result = Utils.bsearch(arrayOfStr, search_for)
expect(result).to.be.equal(1);
});
it('can find Person in an array of Persons', () => {
// This one uses Person.compare() to do the search.
// The previous two tests used the fallback technique.
let search_for = {name:'Fred',length:6};
let result = Utils.bsearch(arrayOfPersons, search_for)
expect(result).to.be.equal(1);
});
});
The error message is:
TS2345: Argument of type 'number[]' is not assignable to parameter of type '{ compare?: Comparator | undefined; }[]'.   Type 'number' has no properties in common with type '{ compare?: Comparator | undefined; }'.
I would appreciate pointers to other techniques if there is a better way to accomplish this (I'm still a TypeScript newbie).
Your generic is:
Type extends { compare?: Comparator }
Which means that Type must fulfill { compare?: Comparator } type. While passing object value, for example { name: 'Barney', length: 2, comparator: /* snip */}, is obviously correct, it's not the case for primitives like 10 and Bob. You need to include information about primitive types in the generic, for example:
Type extends ({ compare?: Comparator }) | number | string
Also, you'd probably want to enrich a bit the object typing:
{[key: string]: unknown, compare?: () => void } | number | string
Because, based on your description, you'd also want to accept also objects that do not have compare function in their type signature at all. If it does sound strange, I recommend reading about excess property checking.

Query parameters from pydantic model

Is there a way to convert a pydantic model to query parameters in fastapi?
Some of my endpoints pass parameters via the body, but some others pass them directly in the query. All this endpoints share the same data model, for example:
class Model(BaseModel):
x: str
y: str
I would like to avoid duplicating my definition of this model in the definition of my "query-parameters endpoints", like for example test_query in this code:
class Model(BaseModel):
x: str
y: str
#app.post("/test-body")
def test_body(model: Model): pass
#app.post("/test-query-params")
def test_query(x: str, y: str): pass
What's the cleanest way of doing this?
The documentation gives a shortcut to avoid this kind of repetitions. In this case, it would give:
from fastapi import Depends
#app.post("/test-query-params")
def test_query(model: Model = Depends()): pass
This will allow you to request /test-query-params?x=1&y=2 and will also produce the correct OpenAPI description for this endpoint.
Similar solutions can be used for using Pydantic models as form-data descriptors.
Special case that isn't mentioned in the documentation for Query Parameters Lists, for example with:
/members?member_ids=1&member_ids=2
The answer provided by #cglacet will unfortunately ignore the array for such a model:
class Model(BaseModel):
member_ids: List[str]
You need to modify your model like so:
class Model(BaseModel):
member_ids: List[str] = Field(Query([]))
Answer from #fnep on GitHub here
This solution is very apt if your schema is "minimal".
But, when it comes to a complicated one like this, Set description for query parameter in swagger doc using Pydantic model, it is better to use a "custom dependency class"
from fastapi import Depends, FastAPI, Query
app = FastAPI()
class Model:
def __init__(
self,
y: str,
x: str = Query(
default='default for X',
title='Title for X',
deprecated=True
)
):
self.x = x
self.y = y
#app.post("/test-body")
def test_body(model: Model = Depends()):
return model
If you are using this method, you will have more control over the OpenAPI doc.
#cglacet 's answer is simple and works, but it will raise pydantic ValidationError when validation fail and not gonna pass the error to client.
You can find reason here.
This works and pass message to client. Code from here.
import inspect
from fastapi import Query, FastAPI, Depends
from pydantic import BaseModel, ValidationError
from fastapi.exceptions import RequestValidationError
class QueryBaseModel(BaseModel):
def __init_subclass__(cls, *args, **kwargs):
field_default = Query(...)
new_params = []
for field in cls.__fields__.values():
default = Query(field.default) if not field.required else field_default
annotation = inspect.Parameter.empty
new_params.append(
inspect.Parameter(
field.alias,
inspect.Parameter.POSITIONAL_ONLY,
default=default,
annotation=annotation,
)
)
async def _as_query(**data):
try:
return cls(**data)
except ValidationError as e:
raise RequestValidationError(e.raw_errors)
sig = inspect.signature(_as_query)
sig = sig.replace(parameters=new_params)
_as_query.__signature__ = sig # type: ignore
setattr(cls, "as_query", _as_query)
#staticmethod
def as_query(parameters: list) -> "QueryBaseModel":
raise NotImplementedError
class ParamModel(QueryBaseModel):
start_datetime: datetime
app = FastAPI()
#app.get("/api")
def test(q_param: ParamModel: Depends(ParamModel.as_query))
start_datetime = q_param.start_datetime
...
return {}

How to get the correct signatures order of annotations in methods when performing overriding

I am trying to fix some methods annotations on magic and normal methods. For example, I have some cases like:
```
class Headers(typing.Mapping[str, str]):
...
def __contains__(self, key: str) -> bool:
...
return False
def keys(self) -> typing.List[str]:
...
return ['a', 'b']
```
and when I run mypy somefile.py --disallow-untyped-defs I have the following errors:
error: Argument 1 of "__contains__" incompatible with supertype "Mapping"
error: Argument 1 of "__contains__" incompatible with supertype "Container"
error: Return type of "keys" incompatible with supertype "Mapping"
What I understand is that I need to override the methods using the #override decorator and I need to respect the order of inheritance. Is it correct?
If my assumption is correct, Is there any place in which I can find the exact signatures of the parent classes?
After asking the question on mypy, the answer was:
Subclassing typing.Mapping[str, str], I'd assume that the function
signature for the argument key in contains ought to match the
generic type?
contains isn't a generic method -- it's defined to have the type signature contains(self, key: object) -> bool. You can check this on typeshed. The reason why contains is defined this way is because doing things like 1 in {"foo": "bar"} is technically legal.
Subclassing def contains(self, key) to def contains(self, key:
str) is in any case more specific. A more specific subtype doesn't
violate Liskov, no?
When you're overriding a function, it's ok to make the argument types more general and the return types more specific. That is, the argument types should be contravariant and the return types covariant.
If we did not follow the rule, we could end up introducing bugs in our code. For example:
class Parent:
def foo(self, x: object) -> None: ...
class Child(Parent):
def foo(self, x: str) -> None: ...
def test(x: Parent) -> None:
x.foo(300) # Safe if 'x' is actually a Parent, not safe if `x` is actually a Child.
test(Child())
Because we broke liskov, passing in an instance of Child into test ended up introducing a bug.
Basically if I use Any for key on __contains__ method is correct and mypy won't complaint :
def __contains__(self, key: typing.Any) -> bool:
...
return False
You can follow the conversation here

Kotlin functional strategy pattern doesn't compile

I'm trying to put several functions in a map. The idea is to have: Map<String, [function]>.
The code is as follows:
class UserIdQueriesHandler {
val strategy: Map<String, KFunction2<#ParameterName(name = "id") String, #ParameterName(name = "options") Array<Options>, Answer>> =
mapOf( // the compiler complains here
"d" to ::debt,
"p" to ::currentProduct
)
fun debt(id: String, options: Array<DebtOptions>): UserDebt = UserDebt(isPresent = true, amount = 0.0)
fun currentProduct(id: String, options: Array<CurrentProductOptions>): UserProducts = UserProducts(products = emptyList())
}
enum class DebtOptions : Options { BOOL, AMOUNT }
enum class CurrentProductOptions : Options { ALL, PRINT, DIGITAL, ENG, HEB, TM }
data class UserDebt(val isPresent: Boolean, val amount: Double) : Answer
data class UserProducts(val products: List<Int>): Answer
Answer and Options are simple kotlin interfaces:
interface Answer
interface Options
Compiler output:
Type inference failed. Expected type mismatch:
required:
Map<String, KFunction2<#ParameterName String, #ParameterName Array<Options>, Answer>>
found:
Map<String, KFunction2<#ParameterName String, {[#kotlin.ParameterName] Array & [#kotlin.ParameterName] Array }, Answer>>
The type of strategy says functions you put into it can accept any Array<Options> as the second argument, which debt and currentProduct can't.
The simplest workaround would be to change their argument type to Array<Options> (or List<Options>; they probably don't need to mutate it!) and fail at runtime if wrong options are passed or ignore them.
Variance section in the documentation is also relevant.
Since an Array can be both read and written, its type parameter is invariant. This makes it so that you can't assign an Array<DebtOptions> to a variable that has the type of Array<Options>. The former isn't a subtype of the latter, because it would allow you to put other elements in the array that are Options, but not DebtOptions, leading to problems to code that has a reference to this array as an Array<DebtOptions>.
A solution would be to make your functions accept Array<Options>, if you can.
val strategy: Map<String, KFunction2<String, Array<Options>, Answer>> =
mapOf(
"d" to ::debt,
"p" to ::currentProduct
)
fun debt(id: String, options: Array<Options>): UserDebt = ...
fun currentProduct(id: String, options: Array<Options>): UserProducts = ...
You could combine this with using the nicer functional type instead of a KFunction2:
val strategy: Map<String, (String, Array<Options>) -> Answer> = ...

Resources