Python - mock function and assert exception - python

I built an API with FastAPI that interacts with DynamoDB.
In the beginning of my journey in Test Driven Development, I have doubts about what to mock.
This is the get method, main.py:
router = FastAPI()
#router.get("/{device_id}")
def get_data(request: Request, device_id: str, query: DataQuery = Depends(DataQuery.depends)):
da_service = DaService()
try:
start_time, end_time = DaService.validate_dates(query.start, query.end)
return 'OK'
except WrongDataFormat as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail='Internal Server Error')
In the test file I started by creating the success test, test_main.py:
from fastapi.testclient import TestClient
from unittest import mock
from utils.exceptions import WrongDataFormat
from endpoints.datalake import router
client = TestClient(router)
def test_success_response():
with mock.patch('endpoints.datalake.DataApiService.get_datalake_data'):
response = client.get('/xxxxx', params = {'start': '1629886483', 'end': '1629886504'})
assert response.status_code == 200
assert isinstance(response.json(), dict)
Now I want to create the test for when the exception WrongDataFormat is returned, but I'm not succeeding... This is what I have right now:
def test_exception_response_():
response = client.get('/xxxxx', params = {'2021-08-28', 'end': '2021-12-25'})
assert response.status_code == 400
How can I mock the function main.validate_dates to return the exception WrongDataFormat and assert it correctly?

If you want to test the status code and message of a response you have to use TestClient(app) where app is the FastAPI application. Converting the exception into the appropriate response is the task of the application, not the router (which is what you're testing with).
client = TestClient(app)
This way you can test the API of your application (which is the most useful surface to test, imho).

Related

Flask Error handling not working properly

I have a custom exception shown below where functions are raising these exceptions.
class UnauthorizedToSendException(HTTPException):
code = 400
description = 'Unauthorized to Send.'
They are then defined in Flask's create_app() as follows:
def handle_custom_exceptions(e):
response = {"error": e.description, "message": ""}
if len(e.args) > 0:
response["message"] = e.args[0]
# Add some logging so that we can monitor different types of errors
app.logger.error(f"{e.description}: {response['message']}")
return jsonify(response), e.code
app.register_error_handler(UnauthorizedToSendException, handle_custom_exceptions)
When this exception is raised below:
class LaptopStatus(Resource):
#staticmethod
def get():
raise UnauthorizedToSendException('you are unauthorized to send')
However, the output is always this way:
{
"message": "you are unauthorized to send"
}
Is there something missing here?
flask_restful.Api has its own error handling implementation that mostly replaces Flask's errorhandler functionality, so using the Flask.errorhandler decorator or Flask.register_error_handler won't work.
There are a couple solutions to this.
Don't Inherit from HTTPException and set PROPAGATE_EXCEPTIONS
The flask-restful error routing has short circuits for handling exceptions that don't inherit from HTTPException, and if you set your Flask app to propagate exceptions, it will send the exception to be handled by normal Flask's handlers.
import flask
from flask import Flask
from flask_restful import Resource, Api
app = Flask(__name__)
# Note that this doesn't inherit from HTTPException
class UnauthorizedToSendException(Exception):
code = 400
description = 'Unauthorized to Send'
#app.errorhandler(UnauthorizedToSendException)
def handle_unauth(e: Exception):
rsp = {"error": e.description, "message": ""}
if len(e.args) > 0:
rsp["message"] = e.args[0]
app.logger.error(f"{e.description}: {rsp['message']}")
return flask.jsonify(rsp), e.code
class LaptopStatus(Resource):
#staticmethod
def get():
raise UnauthorizedToSendException("Not authorized")
api = Api(app)
api.add_resource(LaptopStatus, '/status')
if __name__ == "__main__":
# Setting this is important otherwise your raised
# exception will just generate a regular exception
app.config['PROPAGATE_EXCEPTIONS'] = True
app.run()
Running this I get...
#> python flask-test-propagate.py
# ... blah blah ...
#> curl http://localhost:5000/status
{"error":"Unauthorized to Send","message":"Not authorized"}
Replace flask_restul.Api.error_router
This will override the behavior of the Api class's error_router method, and just use the original handler that Flask would use.
You can see it's just a monkey-patch of the class's method with...
Api.error_router = lambda self, hnd, e: hnd(e)
This will allow you to subclass HTTPException and override flask-restful's behavior.
import flask
from flask import Flask
from flask_restful import Resource, Api
from werkzeug.exceptions import HTTPException
# patch the Api class with a custom error router
# that just use's flask's handler (which is passed in as hnd)
Api.error_router = lambda self, hnd, e: hnd(e)
app = Flask(__name__)
class UnauthorizedToSendException(HTTPException):
code = 400
description = 'Unauthorized to Send'
#app.errorhandler(UnauthorizedToSendException)
def handle_unauth(e: Exception):
print("custom!")
rsp = {"error": e.description, "message": ""}
if len(e.args) > 0:
rsp["message"] = e.args[0]
app.logger.error(f"{e.description}: {rsp['message']}")
return flask.jsonify(rsp), e.code
class LaptopStatus(Resource):
#staticmethod
def get():
raise UnauthorizedToSendException("Not authorized")
api = Api(app)
api.add_resource(LaptopStatus, '/status')
if __name__ == "__main__":
app.run()
Switch to using flask.views.MethodView
Documentation:
flask.views.MethodView
Just thought of this, and it's a more drastic change to your code, but flask has facilities for making building REST APIs easier. flask-restful isn't exactly abandoned, but the pace of changes have slowed down the last several years, and various components like the error handling system have become too inflexible.
If you're using flask-restful specifically for the Resource implementation, you can switch to the MethodView, and then you don't need to do any kind of workarounds.
import flask
from flask import Flask
from flask.views import MethodView
from werkzeug.exceptions import HTTPException
app = Flask(__name__)
class UnauthorizedToSendException(HTTPException):
code = 400
description = 'Unauthorized to Send'
#app.errorhandler(UnauthorizedToSendException)
def handle_unauth(e: Exception):
rsp = {"error": e.description, "message": ""}
if len(e.args) > 0:
rsp["message"] = e.args[0]
app.logger.error(f"{e.description}: {rsp['message']}")
return flask.jsonify(rsp), e.code
class LaptopStatusApi(MethodView):
def get(self):
raise UnauthorizedToSendException("Not authorized")
app.add_url_rule("/status", view_func=LaptopStatusApi.as_view("laptopstatus"))
if __name__ == "__main__":
app.run()
read the https://flask.palletsprojects.com/en/2.1.x/errorhandling/
but: See werkzeug.exception for default HTTPException classes.
For HTTP400 (werkzeug.exception.BadRequest)

Unit Testing Replace remote API Server with predefined response

So, I have a server running FastAPI which will make a API call to a remote API upon request.
I am developping unit-testing for this application, but here comes the question:
Can I, for the purpose of the test, replace a legit remote API server response by a predefined response ?
Example of the tests runned:
from fastapi.testclient import TestClient
from web_api import app
client = TestClient(app)
def test_get_root():
response = client.get('/')
assert response.status_code == 200
assert response.json() == {"running": True}
And the my server
from fastapi import FastAPI
app = FastAPI()
#app.get("/")
def home():
return {"running": True}
This is a simple example, but on other endpoints of my API I would call an external remote API
def call_api(self, endpoint:str, params:dict):
url = self.BASEURL + urllib.parse.quote(endpoint)
try:
response = requests.get(url, params=params)
response.raise_for_status()
except requests.exceptions.HTTPError as error:
print(error)
return response
Because I want to test the response of MY API, I would like to replace the remote API with a predefined response.
Also, one user request can end-up in multiple background API requests with transformed pieces of data.
Edit
Here are some more details on the structure of the application:
#app.get("/stuff/.......",
# lots of params
)
def get_stuff_from_things(stuff:list, params):
api = API(api_key=...)
# Do some stuff with the params
things = generate_things_list(params)
api.search_things(params)
# Check the result
# do some other stuff
return some_response
class API:
BASE_URL = 'https://api.example.com/'
def search_things(self, params):
# Do some stuff
# like putting stuff in the params
for s in stuff:
s.update(self.get_thing(params)) # -> get_thing()
# Do some more stuff
return stuff
# get_thing <- search_things
def get_thing(self, params...):
# Some stuff
results = self.call_api('something', params) # -> call_api()
json = results.json()
# Some more stuff
things = []
for thing in json['things']:
t = Thing(thing)
things.append(t)
return things
# call_api <- get_thing
def call_api(self, endpoint:str, params:dict):
url = self.BASEURL + urllib.parse.quote(endpoint)
try:
response = requests.get(url, params=params)
response.raise_for_status()
except requests.exceptions.HTTPError as error:
print(error)
self.last_response = response
return response
Nb. That is pseudo-code, I simplified the functions by removing the parameters, etc.
I hope it is clear, thanks for your help.
A complex API method might look like this (please pay attention to the depends mechanism - it is crucial):
import urllib
import requests
from fastapi import FastAPI, Depends
app = FastAPI()
# this can be in a different file
class RemoteCallWrapper:
def call_api(self, baseurl: str, endpoint: str, params: dict):
url = baseurl + urllib.parse.quote(endpoint)
try:
response = requests.get(url, params=params)
response.raise_for_status()
except requests.exceptions.HTTPError as error:
print(error)
return response
#app.get("/complex_api")
def calls_other_api(remote_call_wrapper=Depends(RemoteCallWrapper)):
response = remote_call_wrapper.call_api("https://jsonplaceholder.typicode.com",
"/todos/1", None)
return {"result": response.json()}
Now, we wish to replace the remote call class. I wrote a helper library that simplifies the replacement for tests - pytest-fastapi-deps:
from fastapi.testclient import TestClient
from mock.mock import Mock
from requests import Response
from web_api import app, RemoteCallWrapper
client = TestClient(app)
class MyRemoteCallWrapper:
def call_api(self, baseurl: str, endpoint: str, params: dict):
the_response = Mock(spec=Response)
the_response.json.return_value = {"my": "response"}
return the_response
def test_get_root(fastapi_dep):
with fastapi_dep(app).override({RemoteCallWrapper: MyRemoteCallWrapper}):
response = client.get('/complex_api')
assert response.status_code == 200
assert response.json() == {"result": {"my": "response"}}
You override the RemoteCallWrapper with your MyRemoteCallWrapper implementation for the test, which has the same spec.
As asserted - the response changed to our predefined response.
It sounds like you'd want to mock your call_api() function.
With a small modification to call_api() (returning the result of .json()), you can easily mock the whole function while calling the endpoint in your tests.
I'll use two files, app.py and test_app.py, to demonstrate how I would do this:
# app.py
import requests
import urllib
from fastapi import FastAPI
app = FastAPI()
def call_api(self, endpoint: str, params: dict):
url = self.BASEURL + urllib.parse.quote(endpoint)
try:
response = requests.get(url, params=params)
response.raise_for_status()
except requests.exceptions.HTTPError as error:
print(error)
return response.json() # <-- This is the only change. Makes it easier to test things.
#app.get("/")
def home():
return {"running": True}
#app.get("/call-api")
def make_call_to_external_api():
# `endpoint` and `params` could be anything here and could be different
# depending on the query parameters when calling this endpoint.
response = call_api(endpoint="something", params={})
# Do something with the response...
result = response["some_parameter"]
return result
# test_app.py
from unittest import mock
from fastapi import status
from fastapi.testclient import TestClient
import app as app_module
from app import app
def test_call_api_endpoint():
test_response = {
"some_parameter": "some_value",
"another_parameter": "another_value",
}
# The line below will "replace" the result of `call_api()` with whatever
# is given in `return_value`. The original function is never executed.
with mock.patch.object(app_module, "call_api", return_value=test_response) as mock_call:
with TestClient(app) as client:
res = client.get("/call-api")
assert res.status_code == status.HTTP_200_OK
assert res.json() == "some_value"
# Make sure the function has been called with the right parameters.
# This could be dynamic based on how the endpoint has been called.
mock_call.assert_called_once_with(endpoint="something", params={})
If app.py and test_app.py are in the same directory you can run the tests simply by running pytest inside that directory.

How to test httpError in fastAPI with pytest?

I'm trying make some api server with FastAPI.
I have one endpoint named /hello on my project, which gives:
{msg : "Hello World"}
with JSON format when 200 status.
However, It gives error msg when request fails.
Quite simple service. However, I want to test both cases, just for my study. So I also made test code with pytest.
Now I want to know: how can I raise HTTPException and test it on purpose?
#main.py (FAST API)
#app.get('/hello')
def read_main():
try:
return {"msg":"Hello World"}
except requests.exceptions.HTTPError as e:
raise HTTPException(status_code=400,detail='error occured')
#test.py
from fastapi.testclient import TestClient
client = TestClient(app)
# This test works
def test_read_main():
response = client.get("/hello")
assert response.json() == {"msg":"Hello World"}
assert response.status_code == 200
def test_errors():
# How can I test except in endpoint "/hello" ?
# The code below never works as I expect
# with pytest.raises(HTTPException) as e:
# raise client.get("/hello").raise_for_status()
# print(e.value)
The problem here is that your logic is way to simplistic to test. As luk2302 said; in the current form, your except block is never called and thus can never be tested. Replacing your logic with something more testable, allows us to force an Exception being thrown.
File: app.py
from fastapi import FastAPI
from fastapi.exceptions import HTTPException
import requests
app = FastAPI()
#We've put this in a seperate function so we can mock this.
def get_value():
return {"msg":"Hello World"}
#app.get('/hello')
def read_main():
try:
return get_value()
except requests.exceptions.HTTPError as e:
raise HTTPException(status_code=400,detail='error occured')
Note that the return value of your endpoint is now actually provided by the get_value() function.
The test.py file would look like this:
from fastapi import HTTPException
import app
from fastapi.testclient import TestClient
import requests
from pytest_mock import MockerFixture
client = TestClient(app.app)
def test_read_main():
response = client.get("/hello")
assert response.json() == {"msg":"Hello World"}
assert response.status_code == 200
def get_value_raise():
raise requests.exceptions.HTTPError()
def test_errors(mocker: MockerFixture):
mocker.patch("app.get_value", get_value_raise)
response = client.get("/hello")
assert response.status_code == 400
assert response.json() == {"detail": "error occured"}
Note that we replace the app.get_value function with a function that will definitely raise the type of exception that you are catching in your application logic. The response of the test client is (however) just an HTTP response, but with statuscode 400 and a detail in the json body. We assert for that.
The result:
(.venv) jarro#MBP-van-Jarro test_http_exception % pytest test.py
=================================================== test session starts ===================================================
platform darwin -- Python 3.10.4, pytest-7.1.2, pluggy-1.0.0
rootdir: /Users/jarro/Development/fastapi-github-issues/SO/test_http_exception
plugins: anyio-3.6.1, mock-3.8.2
collected 2 items
test.py .. [100%]
==================================================== 2 passed in 0.17s ====================================================
I used pytest, and by extension I used pytest-mocker to mock the get_value function.

How to test that a model was used in a FastAPI route?

I'm trying to check if a specific model was used as an input parser for a FastAPI route. However, I'm not sure how to patch (or spy on) it.
I have the following file structure:
.
└── roo
├── __init__.py
├── main.py
└── test_demo.py
main.py:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class ItemModel(BaseModel):
name: str
#app.post("/")
async def read_main(item: ItemModel):
return {"msg": f"Item: {item.name}"}
test_demo.py:
from fastapi.testclient import TestClient
from unittest.mock import patch
from roo.main import app, ItemModel
client = TestClient(app)
def test_can_creating_new_item_users_proper_validation_model():
with patch('roo.main.ItemModel', wraps=ItemModel) as patched_model:
response = client.post("/", json={'name': 'good'})
assert response.status_code == 200
assert response.json() == {"msg": "Item: good"}
assert patched_model.called
However, patched_model is never called (other asserts pass). I don't want to change the functionality or replace ItemModel in main.py, I just want to check if it was used.
My first approach to this was to wrap the read_main method and check that the item passed into the function is indeed an instance of ItemModel. But that was a dead-end approach because of the way FastAPI endpoints are prepared and stored: FastAPI stores a copy of the endpoint function objects in a list: (see fastapi/routing.py), then evaluates at request-time which endpoint to call.
from roo.main import app
def test_read_main():
assert 'read_main' in [r.endpoint.__name__ for r in app.routes]
# check that read_main was called *and* received an ItemModel instance?
My second approach involves spying or "breaking" the initialization of ItemModel, such that if the endpoint does indeed use that model, then a "broken" ItemModel would cause a request that hits that endpoint to fail. We "break" ItemModel by making use of the fact that (1) FastAPI calls the __init__ of your model during the request-response cycle, and (2) a 422 error response is propagated by default when the endpoint is unable to serialize a model properly:
class ItemModel(BaseModel):
name: str
def __init__(__pydantic_self__, **data: Any) -> None:
print("Make a POST request and confirm that this is printed out")
super().__init__(**data)
So in tests, just mock the __init__ method:
Example for pytest
import pytest
from fastapi.testclient import TestClient
from roo.main import app, ItemModel
def test_read_main(monkeypatch: pytest.MonkeyPatch):
client = TestClient(app)
def broken_init(self, **data):
pass # `name` and other fields won't be set
monkeypatch.setattr(ItemModel, '__init__', broken_init)
with pytest.raises(AttributeError) as exc:
client.post("/", json={'name': 'good'})
assert 422 == response.status_code
assert "'ItemModel' object has no attribute" in str(exc.value)
Example for pytest + pytest-mock's mocker.spy
from fastapi.testclient import TestClient
from pytest_mock import MockerFixture
from roo.main import app, ItemModel
def test_read_main(mocker: MockerFixture):
client = TestClient(app)
spy = mocker.spy(ItemModel, '__init__')
client.post("/", json={'name': 'good'})
spy.assert_called()
spy.assert_called_with(**{'name': 'good'})
Example for unittest
from fastapi.testclient import TestClient
from roo.main import app, ItemModel
from unittest.mock import patch
def test_read_main():
client = TestClient(app)
# Wrapping __init__ like this isn't really correct, but serves the purpose
with patch.object(ItemModel, '__init__', wraps=ItemModel.__init__) as mocked_init:
response = client.post("/", json={'name': 'good'})
assert 422 == response.status_code
mocked_init.assert_called()
mocked_init.assert_called_with(**{'name': 'good'})
Again, the tests check that the endpoint fails in either serializing into an ItemModel or in accessing item.name, which will only happen if the endpoint is indeed using ItemModel.
If you modify the endpoint from item: ItemModel into item: OtherModel:
class OtherModel(BaseModel):
name: str
class ItemModel(BaseModel):
name: str
#app.post("/")
async def read_main(item: OtherModel): # <----
return {"msg": f"Item: {item.name}"}
then running the tests should now fail because the endpoint is now creating the wrong object:
def test_read_main(mocker: MockerFixture):
client = TestClient(app)
spy = mocker.spy(ItemModel, '__init__')
client.post("/", json={'name': 'good'})
> spy.assert_called()
E AssertionError: Expected '__init__' to have been called.
test_demo_spy.py:11: AssertionError
with pytest.raises(AttributeError) as exc:
response = client.post("/", json={'name': 'good'})
> assert 422 == response.status_code
E assert 422 == 200
E +422
E -200
test_demo_pytest.py:15: AssertionError
The assertion errors for 422 == 200 is a bit confusing, but it basically means that even though we "broke" ItemModel, we still got a 200/OK response.. which means ItemModel is not being used.
Likewise, if you modified the tests first and mocked-out the __init__ of OtherModel instead of ItemModel, then running the tests without modifying the endpoint will result in similar failing tests:
def test_read_main(mocker: MockerFixture):
client = TestClient(app)
spy = mocker.spy(OtherModel, '__init__')
client.post("/", json={'name': 'good'})
> spy.assert_called()
E AssertionError: Expected '__init__' to have been called.
def test_read_main():
client = TestClient(app)
with patch.object(OtherModel, '__init__', wraps=OtherModel.__init__) as mocked_init:
response = client.post("/", json={'name': 'good'})
# assert 422 == response.status_code
> mocked_init.assert_called()
E AssertionError: Expected '__init__' to have been called.
The assertion here is less confusing because it says we expected that the endpoint will call OtherModel's __init__, but it wasn't called. It should pass after modifying the endpoint to use item: OtherModel.
One last thing to note is that since we are manipulating the __init__, then it can cause the "happy path" to fail, so it should now be tested separately. Make sure to undo/revert the mocks and patches:
Example for pytest
def test_read_main(monkeypatch: pytest.MonkeyPatch):
client = TestClient(app)
def broken_init(self, **data):
pass
# Are we really using ItemModel?
monkeypatch.setattr(ItemModel, '__init__', broken_init)
with pytest.raises(AttributeError) as exc:
response = client.post("/", json={'name': 'good'})
assert 422 == response.status_code
assert "'ItemModel' object has no attribute" in str(exc.value)
# Okay, really using ItemModel. Does it work correctly?
monkeypatch.undo()
response = client.post("/", json={'name': 'good'})
assert response.status_code == 200
assert response.json() == {"msg": "Item: good"}
Example for pytest + pytest-mock's mocker.spy
from pytest_mock import MockerFixture
from fastapi.testclient import TestClient
from roo.main import app, ItemModel
def test_read_main(mocker: MockerFixture):
client = TestClient(app)
# Are we really using ItemModel?
spy = mocker.spy(ItemModel, '__init__')
client.post("/", json={'name': 'good'})
spy.assert_called()
spy.assert_called_with(**{'name': 'good'})
# Okay, really using ItemModel. Does it work correctly?
mocker.stopall()
response = client.post("/", json={'name': 'good'})
assert response.status_code == 200
assert response.json() == {"msg": "Item: good"}
Example for unittest
def test_read_main():
client = TestClient(app)
# Are we really using ItemModel?
with patch.object(ItemModel, '__init__', wraps=ItemModel.__init__) as mocked_init:
response = client.post("/", json={'name': 'good'})
assert 422 == response.status_code
mocked_init.assert_called()
mocked_init.assert_called_with(**{'name': 'good'})
# Okay, really using ItemModel. Does it work correctly?
response = client.post("/", json={'name': 'good'})
assert response.status_code == 200
assert response.json() == {"msg": "Item: good"}
All in all, you might want to consider if/why it's useful to check for which model is exactly used. Normally, I just check that passing-in valid request params returns the expected valid response, and likewise, that invalid requests returns an error response.

How to return 400 (Bad Request) on Flask?

I have created a simple flask app that and I'm reading the response from python as:
response = requests.post(url,data=json.dumps(data), headers=headers )
data = json.loads(response.text)
Now my issue is that under certain conditions I want to return a 400 or 500 message response. So far I'm doing it like this:
abort(400, 'Record not found')
#or
abort(500, 'Some error...')
This does print the message on the terminal:
But in the API response I kept getting a 500 error response:
The structure of the code is as follows:
|--my_app
|--server.py
|--main.py
|--swagger.yml
Where server.py has this code:
from flask import render_template
import connexion
# Create the application instance
app = connexion.App(__name__, specification_dir="./")
# read the swagger.yml file to configure the endpoints
app.add_api("swagger.yml")
# Create a URL route in our application for "/"
#app.route("/")
def home():
"""
This function just responds to the browser URL
localhost:5000/
:return: the rendered template "home.html"
"""
return render_template("home.html")
if __name__ == "__main__":
app.run(host="0.0.0.0", port="33")
And main.py has all the function I'm using for the API endpoints.
E.G:
def my_funct():
abort(400, 'Record not found')
When my_funct is called, I get the Record not found printed on the terminal, but not in the response from the API itself, where I always get the 500 message error.
You have a variety of options:
The most basic:
#app.route('/')
def index():
return "Record not found", 400
If you want to access the headers, you can grab the response object:
#app.route('/')
def index():
resp = make_response("Record not found", 400)
resp.headers['X-Something'] = 'A value'
return resp
Or you can make it more explicit, and not just return a number, but return a status code object
from flask_api import status
#app.route('/')
def index():
return "Record not found", status.HTTP_400_BAD_REQUEST
Further reading:
You can read more about the first two here: About Responses (Flask quickstart)
And the third here: Status codes (Flask API Guide)
I like to use the flask.Response class:
from flask import Response
#app.route("/")
def index():
return Response(
"The response body goes here",
status=400,
)
flask.abort is a wrapper around werkzeug.exceptions.abort which is really just a helper method to make it easier to raise HTTP exceptions. That's fine in most cases, but for restful APIs, I think it may be better to be explicit with return responses.
Here's some snippets from a Flask app I wrote years ago. It has an example of a 400 response
import werkzeug
from flask import Flask, Response, json
from flask_restplus import reqparse, Api, Resource, abort
from flask_restful import request
from flask_cors import CORS
app = Flask(__name__)
CORS(app)
api = Api(app)
parser = reqparse.RequestParser()
parser.add_argument('address_to_score', type=werkzeug.datastructures.FileStorage, location='files')
class MissingColumnException(Exception):
pass
class InvalidDateFormatException(Exception):
pass
#api.route('/project')
class Project(Resource):
#api.expect(parser)
#api.response(200, 'Success')
#api.response(400, 'Validation Error')
def post(self):
"""
Takes in an excel file of addresses and outputs a JSON with scores and rankings.
"""
try:
df, input_trees, needed_zones = data.parse_incoming_file(request)
except MissingColumnException as e:
abort(400, 'Excel File Missing Mandatory Column(s):', columns=str(e))
except Exception as e:
abort(400, str(e))
project_trees = data.load_needed_trees(needed_zones, settings['directories']['current_tree_folder'])
df = data.multiprocess_query(df, input_trees, project_trees)
df = data.score_locations(df)
df = data.rank_locations(df)
df = data.replace_null(df)
output_file = df.to_dict('index')
resp = Response(json.dumps(output_file), mimetype='application/json')
resp.status_code = 200
return resp
#api.route('/project/health')
class ProjectHealth(Resource):
#api.response(200, 'Success')
def get(self):
"""
Returns the status of the server if it's still running.
"""
resp = Response(json.dumps('OK'), mimetype='application/json')
resp.status_code = 200
return resp
You can return a tuple with the second element being the status (either 400 or 500).
from flask import Flask
app = Flask(__name__)
#app.route('/')
def hello():
return "Record not found", 400
if __name__ == '__main__':
app.run()
Example of calling the API from python:
import requests
response = requests.get('http://127.0.0.1:5000/')
response.text
# 'This is a bad request!'
response.status_code
# 400
I think you're using the abort() function correctly. I suspect the issue here is that an error handler is that is catching the 400 error and then erroring out which causes the 500 error. See here for more info on flask error handling.
As an example, the following would change a 400 into a 500 error:
#app.errorhandler(400)
def handle_400_error(e):
raise Exception("Unhandled Exception")
If you're not doing any error handling, it could be coming from the connexion framework, although I'm not familiar with this framework.
You can simply use #app.errorhandler decorator.
example:
#app.errorhandler(400)
def your_function():
return 'your custom text', 400

Categories

Resources