I have built an API using FastAPI and am trying to send data to it from a client.
Both the API and the client use a similar Pydantic model for the data that I want to submit. This includes a field that contains a file path, which I store in a field of type pathlib.path.
However, FastAPI does not accept the submission because it apparently cannot handle the path object:
TypeError: Object of type PosixPath is not JSON serializable
Here's a minimal test file that shows the problem:
import pathlib
from pydantic import BaseModel
from fastapi import FastAPI
from fastapi.testclient import TestClient
api = FastAPI()
client = TestClient(api)
class Submission(BaseModel):
file_path: pathlib.Path
#api.post("/", response_model=Submission)
async def add_submission(subm: Submission):
print(subm)
# add submission to database
return subm
def test_add_submission():
data = {"file_path": "/my/path/to/file.csv"}
print("original data:", data)
# create a Submission object, which casts filePath to pathlib.Path:
submission = Submission(**data)
print("submission object:", submission)
payload = submission.dict()
print("payload:", payload)
response = client.post("/", json=payload) # this throws the error
assert response.ok
test_add_submission()
When I change the model on the client side to use a string instead of a Path for file_path, things go through. But then I lose the pydantic power of casting the input to a Path when a Submission object is created, and then having a Path attribute with all its possibilities. Surely, there must be better way?
What is the correct way to send a pathlib.PosixPath object to a FastAPI API as part of the payload?
(This is Python 3.8.9, fastapi 0.68.1, pydantic 1.8.2 on Ubuntu)
The problem with you code is, that you first transform the pydantic model into a dict, which you then pass to the client, which uses its own json serializer and not the one provided by pydantic.
submission.dict() converts any pydantic model into a dict but keeps any other datatype.
With client.post("/", json=payload) requests json serializer is used, which cannot handle pathlib.Path.
The solution is, not to convert the pydantic model into dict first, but use the json() method of the pydantic model itself and pass it to the client.
response = client.post("/", data=submission.json())
Notice that you have to change the parameter json to data.
Related
I'm handling this request in my code (Python3.9, FastAPI, Pydantic):
https://myapi.com/api?params[A]=1¶ms[B]=2
I tried to make following model:
BaseModel for handling special get request
(for fastapi.Query and pydantic.Field is same)
I also set up aliases for it, but in swagger docs I see next field:
Snap of the swagger docs
There are fields that are specified as extra_data
So, if I specify query params in parameters of my endpoint like this:
#app.get('/')
def my_handler(a: str = Query(None, alias="params[A]")):
return None
Everything works fine. How can I fix it? I want to initialize my pydantic.BaseModel with speacial aliases using this way and avoid usage of query-params in
class MyModel(BaseModel):
a = Field(alias="params[A]")
b = Field(alias="params[B]")
def my_handler(model: MyModel = Depends()):
return model.dict()
I am migrating a service from Flask to FastAPI and using Pydantic models to generate the documentation. However, I'm a little unsure about the schema check. I'm afraid there will be some unexpected data (like a different field format) and it will return an error.
In the Pydantic documentation there are ways to create a model without checking the schema: https://pydantic-docs.helpmanual.io/usage/models/#creating-models-without-validation
However, as this is apparently instantiated by FastAPI itself, I don't know how to disable this schema check when returning from FastAPI.
You could return Response directly, or with using custom responses for automatic convertion. In this case, response data is not validated against the response model. But you can still document it as described in Additional Responses in OpenAPI.
Example:
class SomeModel(BaseModel):
num: int
#app.get("/get", response_model=SomeModel)
def handler(param: int):
if param == 1: # ok
return {"num": "1"}
elif param == 2: # validation error
return {"num": "not a number"}
elif param == 3: # ok (return without validation)
return JSONResponse(content={"num": "not a number"})
elif param == 4: # ok (return without validation and conversion)
return Response(content=json.dumps({"num": "not a number"}), media_type="application/json")
You can set the request model as a typing.Dict or typing.List
from typing import Dict
app.post('/')
async def your_function(body: Dict):
return { 'request_body': body}
FastAPI doesn't enforce any kind of validation, so if you don't want it, simply do not use Pydantic models or type hints.
app.get('/')
async def your_function(input_param):
return { 'param': input_param }
# Don't use models or type hints when defining the function params.
# `input_param` can be anything, no validation will be performed.
However, as #Tryph rightly pointed out, since you're using Pydantic to generate the documentation, you could simply use the Any type like so:
from typing import Any
from pydantic import BaseModel
class YourClass(BaseModel):
any_value: Any
Beware that the Any type also accepts None, making in fact the field optional. (See also typing.Any in the Pydantic docs)
I want to generate a description of all available responses (along with code 200 example), which are represented in the code, like here.
from typing import Any
import uvicorn
from fastapi import FastAPI, HTTPException
router = FastAPI()
from pydantic import BaseModel
class FileItemBase(BaseModel):
current_project: str = "Test project"
class FileItemInDBBase(FileItemBase):
id: int
folder_path: str
class Config:
orm_mode = True
class FileResponse(FileItemInDBBase):
pass
#router.get("/", response_model=FileResponse)
def example_code() -> Any:
"""
# beautiful description
to demonstrate functionality
"""
demo=True
if demo:
raise HTTPException(418, "That is a teapot.")
if __name__ =="__main__":
uvicorn.run(router)
What I got with this is such a description.
When I try this out - I got an error response (as expected).
What I want - is the description of an error included in the example responses, like here. A Frontend-developer can look at this description and process such cases in the right way without testing the API.
I know how it can be made within OpenAPI specs.
Is there a way to generate this description with FastAPI?
You can add a responses parameter to your path operation.
Then you can pass your model there. It will create a schema for that model.
class FileItemBase(BaseModel):
current_project: str = "Test project"
#app.get("/", response_model=FileItemBase, responses={418: {"model": FileItemBase}})
def example_code():
"""
# beautiful description
to demonstrate functionality
"""
demo = True
if demo:
raise HTTPException(418, "That is a teapot.")
So I am making Angular 8 and Django project. The scenario is that from a form in Angular, data is sent to Django to store it in a database.
class NewProfileView(viewsets.ModelViewSet):
queryset = NewProfile.objects.all()
serializer_class = NewProfileSerializer
def post(self,request,*args,**kwargs):
email = request.data['email']
password = request.data['password']
username = request.data['username']
NewProfile.objects.create(email=email,password=password,username=username)
return HttpResponse({'message':'Registered Successfully'},status=200)
The above represents my Django view. Now, seeing this, for a successful creation, I should get the response as 'Registered Successfully'. But what I get is the data I submitted in JSON format (basically a dictionary. I don't really know if it is json).
Why is it happening?
export class PostService {
private url = 'http://localhost:8000/login/';
constructor(private httpClient:HttpClient) {}
getPosts(data){
return this.httpClient.get(this.url+'?email='+data.email+'&password='+data.password);
}
create(url,post){
return this.httpClient.post<any>(url,post);
}
}
This is what is there in my angular service.
onSubmit(){
console.log(this.userForm);
this.service.create('http://localhost:8000/profile/',this.userForm)
.subscribe(response=>{
console.log(response);
});
This is the code in my component.ts file.
P.S.- I know I'm storing the passwords wrong way. But its just for debugging purposes.
To return a dict data from Django should use JsonResponse. This will serializer the Dict into json and add the correct Content-Type header.
from django.http import JsonResponse
>>> response = JsonResponse({'foo': 'bar'})
>>> response.content
>>> b'{"foo": "bar"}' # This will be the body sent to the client
# In your Case
>>> return JsonResponse({'message':'Registered Successfully'},status=200)
On the Javascript side, how you access the JSON data will vary based on the client you use to make http call. You can parse the response body using JSON.parse(body), but most http clients I have used will handle that for you (Fetch API has a response.json() method, and I think axios gives you JS object automatically when the response is json type)
Note on Dict vs JSON
the data I submitted in JSON format (basically a dictionary. I don't really know if it is json).
Dictionary is a native python type. Looks similar to JSON but they are not the same. eg. Python uses None, while JSON uses null, and many other differences like that.
JSON (Javascript Object Notation) is a way to serializer and deserializer javascript objects.
To send a dict data to your JS client as a JSON object, you would need to json.dumps(dict) to get the serialized json version of your dict, and then return that in the response body. JsonResponse handles the serialization for your, and also adds the "Content-Type" headers "application/json" to let your client know it's receiving a json body body that can be deserialized into a JS object.
How can I submit a POST request with Django test Client, such that I include form data in it?
In particular, I would like to have something like (inspired by How should I write tests for Forms in Django?):
from django.tests import TestCase
class MyTests(TestCase):
def test_forms(self):
response = self.client.post("/my/form/", {'something':'something'})
My endpoint /my/form has some internal logic to deal with 'something'.
The problem was that when trying to later access request.POST.get('something') I couldn't get anything.
I found a solution so I'm sharing below.
The key was to add content_type to the post method of client, and also urlencode the data.
from urllib import urlencode
...
data = urlencode({"something": "something"})
response = self.client.post("/my/form/", data, content_type="application/x-www-form-urlencoded")
Hope this helps someone!
If you are sending dictionaries on old-django versions using client, you must define the content_type='application/json' because its internal transformation fails to process dictionaries, you also need to send the dictionary like a blob using the json.dumps method. In conclusion, the following must work:
import json
from django.tests import TestCase
class MyTests(TestCase):
def test_forms(self):
response = self.client.post("/my/form/", json.dumps({'something':'something'}), content_type='application/json')
If you provide content_type as application/json, the data is serialized using json.dumps() if it’s a dict, list, or tuple. Serialization is performed with DjangoJSONEncoder by default, and can be overridden by providing a json_encoder argument to Client. This serialization also happens for put(), patch(), and delete() requests.
I have tried unit testing the POST requests in Django using Client(), but I fail to make it work (even with the methods specified above). So here is an alternative approach I take exclusively for the POST requests (using HttpRequest()):
from django.http import HttpRequest
from django.tests import TestCase
from . import views
# If a different test directory is being used to store the test files, replace the dot with the app name
class MyTests(TestCase):
def test_forms(self):
request = HttpRequest()
request.method = 'POST'
request.POST['something'] = 'something'
request.META['HTTP_HOST'] = 'localhost'
response = views.view_function_name(request)
self.assertNotIn(b'Form error message', response.content)
# make more assertions, if needed
Replace the view_function_name() with the actual function name. This function sends a POST request to the view being tested with the form-field 'something' and it's corresponding value. The assertion statements would totally depend on the utility of the test functions, however.
Here are some assertions that may be used:
self.assertEquals(response.status_code, 302):
Make this assertion when the form, upon submission of the POST request, redirects (302 is the status code for redirection). Read more about it here.
self.assertNotIn(b'Form error message', response.content):
Replace 'Form error message' with the error message that the form generates when incorrect details are sent through the request. The test would fail if the test data is incorrect (the text is converted to bytes since HttpResponse().content is a bytes object as well).
If the view function uses the Django Message framework for displaying the form error messages as well, include this before the response:
from django.contrib import messages
...
request._messages = messages.storage.default_storage(request)
If the view function uses Sessions, include this before the response:
from importlib import import_module
from django.conf import settings
...
engine = import_module(settings.SESSION_ENGINE)
session_key = None
request.session = engine.SessionStore(session_key)
Before sending out the request, remember the use of any context-processors that your application may use.
I personally find this method more intuitive (and functional). This seems to cover all possible test cases with regard to HTTP requests and forms as well.
I would also like to suggest that each unit test case could be broken down into separate components for increased coverage and discovering latent bugs in code, instead of clubbing all cases in a single test_forms().
This technique was mentioned by Harry J.W. Percival in his book Test-Driven Development with Python.