Migrating AWS Lamda + APIs to FastAPI + Mangum - python

I have a Lambda function handler which takes in a request, and classify into REST or HTTP based on the call and preprocess it inpreprocess_and_call to go through validation of the call, and finally makes a call to the backend API endpoints.
My current Lambda function (main.py) is set up like this:
# Lambda handler
def handler(event, context):
# if REST call
if event.get('routeKey', None) == "$default" and 'rawPath' in event:
path = event.get('path', None)
# ... parse other parameters such as method, headers, etc ...
# else if HTTP call
elif event.get('path', None):
path = event.get('rawPath', None)
# ... parse other parameters such as method, headers, etc ...
return preprocess_and_call(path, method, headers, query_params, body)
# calls the endpoints after validation, authrization, etc
def preprocess_and_call(path, method, headers, query_params, body):
try:
if path in endpoints_list:
endpoint_to_call = endpoints_list[path]
# ... validation logics here ...
response = endpoint_to_call(path, method, headers, query_params, body)
return response
except Exception as ex:
... return error ...
This is one of the sample endpoints:
def sample_endpoint(path, method, headers, query_params, body):
try:
id = query_paraams.get('id', None)
model = SampleModel()
model.process()
response_body = model.json()
return {
'status_code': 200,
'body': response_body
}
except Exception as ex:
# ... process exception ...
I am trying to move my lambda function + APIs to make use of FastAPI + Mangum and this is what I have so far:
Currently, my lambda handler is set up using FastAPI middleware decorator and Mangum like the following (Mangum works as an adapter for ASGI applications like the ones you can create with fastAPI so that they can send and receive information from API Gateway to Lambda and vice versa. I removed handler because Mangum provides HTTP and REST support):
from routers import router
app = FastAPI()
app.include_router(router)
# Handler for api calls
#app.middleware("http")
async def preprocess_and_call(request: Request, call_next):
try:
if path in endpoints_list:
# ... validation logics here ...
response = await call_next(request) # calls the endpoints with corresponding path
# ... process the response ...
except Exception as ex:
# ... process exception ...
handler = Mangum(app)
And I have several endpoints which gets called from the above lambda function, do their job and return the response accordingly like this:
#router.get("/index")
def sample_endpoint(request: Request):
try:
query_params= request.query_params
# ....
return Response(
status_code = 200,
content=json.dumps(response_body)
)
except Exception as ex:
# ... process exception ...
I've tested this locally, and things work smoothly. Would this be the best way of doing this or are there other/better ways? Any suggestions would be greatly appreciated!

Related

Why doesn't the abort function in Flask take the handlers?

I am developing a REST API with python and flask, I leave the project here Github project
I added error handlers to the application but when I run an abort function, it gives me a default message from Flask, not the structure I am defining.
I will leave the path to the handlers and where I run the abort from.
Handlers abort(400)
Flask message
Ok, the solution was told to me that it could be in another question.
What to do is to overwrite the handler function of the Flask Api object.
With that, you can configure the format with which each query will be answered, even the ones that contain an error.
def response_structure(code_status: int, response=None, message=None):
if code_status == 200 or code_status == 201:
status = 'Success'
else:
status = 'Error'
args = dict()
args['status'] = status
if message is not None:
args['message'] = message
if response is not None:
args['response'] = response
return args, code_status
class ExtendAPI(Api):
def handle_error(self, e):
return response_structure(e.code, str(e))
Once the function is overwritten, you must use this new one to create
users_bp = Blueprint('users', __name__)
api = ExtendAPI(users_bp)
With this, we can then use the flask functions to respond with the structure that we define.
if request.args.get('name') is None:
abort(400)
Response JSON
{
"response": "400 Bad Request: The browser (or proxy) sent a request that this server could not understand.",
"status": "Error"
}

Writing Unit Test for Python Flask Decorator - How to Pass Request

I'm relatively new to pytest so any guidance on the proper way to do this is appreciated. I'm looking for information how to write a unit test for a flask route decorator using pytest.
As an example, I have a flask route that I want to protect with a Bearer token or similar and am doing so through a decorator. I understand I need to pass function and an HTTP request through this decorator but I'm not quite clear on how to do that with pytest.
I thought maybe using a lambda: None to mock a function pass but it doesn't appear to be entering the decorator in that method. In additon I'm not really sure how to assert my return from the decorator.
Decorator
def auth_required(func):
def decorator(*args, **kwargs)
try:
access_token = request.headers.get('AUTHORIZATION', '').split('Bearer ')[1]
except IndexError:
return {"Access Denied": "Invalid authorization header"}, 400
if is_token_valid(access_token):
return func(*args, **kwargs)
else:
return {"Access Denied": "The provided bearer token is not valid"}, 403
return decorator
Usage in Flask Routes
#api.route('/$test-route', methods=['POST'], endpoint="test-route-example")
#auth_required
def process_json():
return "Hello World"
Attempt to Test
class TestTokenRequired:
payload = {'field1': 'value'}
def test_invalid_token(self, app):
encoded_jwt = jwt.encode(self.payload, '12345', algorithm='HS256')
with app.test_request_context(
headers={'AUTHORIZATION': 'Bearer {}'.format(encoded_jwt)}
):
auth_required(lambda: None)()

Disable requires_auth decorator from fastapi-microsoft-identity

I am using https://pypi.org/project/fastapi-microsoft-identity/ package for AD Azure authorization using requires_auth decorator. From my tests would like to avoid the authorization process but I cannot disable this decorator always returns 401.
this is the following code I am using from my test
def mock_decorator(*args, **kwargs) :
def decorator(f)
wraps(f)
def requires_auth(*args, **kwargs):
return f(*args, **kwargs)
return requires_auth
patch('fastapi_microsoft_identity.requires_auth', mock_decorator).start()
#pytest.fixture
def client():
with TestClient(api) as client:
yield client
def test_api_endpoint(client):
response = client.get("/api/weather/london")
assert response.status_code == 200
Since I am patching auth decorator should return 200 status code, instead, I am getting 401 UnAuthorized
The following code from api
#router.get('/api/weather/{city}')
#requires_auth
async def weather(request: Request, loc: Location = Depends(), units: Optional[str] = 'metric'):
try:
validate_scope(expected_scope, request)
return await openweather_service.get_report_async(loc.city, loc.state, loc.country, units)
except AuthError as ae:
return fastapi.Response(content=ae.error_msg, status_code=ae.status_code)
except ValidationError as ve:
return fastapi.Response(content=ve.error_msg, status_code=ve.status_code)
except Exception as x:
return fastapi.Response(content=str(x), status_code=500)
please help what I am doing wrong.
You can't really bypass a decorator but can circumnavigate it if in its implementation it uses #wrap() in the code.
Now # require_auth does use a #wrap in its implementation so we can circumnavigate the decorator by calling the original function in this case ‘weather’ function as:-
weather.__wrapped__()
instead of the usual way of calling the function.
The __wrapped__ method basically has reference to the original function instead of the decorator.
Here I created a small API which basically returns two strings now here I have called the weather function without the .__wrapped__
# main.py
from fastapi_microsoft_identity import requires_auth, validate_scope, AuthError
import fastapi
from fastapi import FastAPI
app = FastAPI()
#requires_auth
async def weather() :
return False
#app.get("/")
async def root():
if await weather.() :
return {"Decorator is working and not allowing the function f() to work "}
else :
return {"Decorator is disabled"}
Here the weather () doesn’t get executed that is why the string “Decorator is working and not allowing the function f() to work” is returned but in your case it is giving unauthorize error
But now when I call it using wrapped
# main.py
from fastapi_microsoft_identity import requires_auth, validate_scope, AuthError
import fastapi
from fastapi import FastAPI
app = FastAPI()
#requires_auth
async def weather() :
return False
#app.get("/")
async def root():
if await weather.__wrapped__() :
return {"Decorator is working and not allowing the function f() to work "}
else :
return {"Decorator is disabled"}
Now that I have used the,__wrapped__ while calling the weather function it is being executed directly circumnavigating the decorator
So if you don’t want the require#authto work call the weather function like this weather.__wrapped__(<arguments>)
Refer this python docs on wrapper method and this gihub repo on require_auth.

How to test functions that request data from external endpoints in django

I am trying to test my functions on my django api that perform external requests to external api. How can
i test the following scenarios: success, failed, and exceptions like timeout
The following is a simplified functionality
def get_quote(*args):
# log request
try:
response = requests.post(url, json=data)
# parse this response
except:
# log file :)
finally:
# log_response(...)
return parsed_response or None
None: response can be success, failed, can timeout. I want to test those kind of scenarios
You can mock the result of calling the external API and set an expected return value in the test function:
from unittest.mock import patch
from django.test import TestCase
class ExternalAPITests(TestCase):
#patch("requests.post")
def test_get_quote(self, mock):
mock.return_value = "predetermined external result"
self.assertEquals("expected return value", get_quote())
You can use the responses package - https://pypi.org/project/responses/
import unittest
import responses
from your_package import get_quote
class TestPackage(unittest.TestCase):
#responses.activate
def test_get_quote(self):
url = "http://some_fake_url.com"
responses.add(responses.POST, url, json={"test": "ok"}, status=200)
self.assertDictEqual({"test": "ok"}, get_quote(url))
#responses.activate
def test_get_quote_with_exception(self):
url = "http://some_fake_url.com"
responses.add(responses.POST, url, body=Exception('...'))
with self.assertRaises(Exception):
get_quote(url)

abort or make_response or jsonify

I'm writing Flask web-application and want to know about best practice for returning unsuccessful response.
Code example:
#app.route("/api/model", methods=["DELETE"])
def delete_models():
"""
Deleting all models.
"""
try:
model_service.delete_all_models()
response = make_response(jsonify(success=True))
except Exception as ex:
response = make_response(jsonify(str(ex)), 500)
response.headers["Content-Type"] = "application/json"
return response
I found theree different approaches.
return jsonify(success=False)
abort(404, description="There is no model with this index!")
response.headers["Content-Type"] = "application/json"
return response```
Which one is the best way? What advantages and disadvantages in each of them?
You can use the error handler decorator of flask, as it explained in the doc.
For example:
#app.errorhandler(InvalidUsage)
def handle_invalid_usage(error):
response = jsonify(error.to_dict())
response.status_code = error.status_code
# Log here the error
return response
# In your exception or error control use:
raise InvalidUsage('This view is gone', status_code=410)

Categories