I'm try to test the api I wrote with Fastapi. I have the following method in my router :
#app.get('/webrecord/check_if_object_exist')
async def check_if_object_exist(payload: WebRecord) -> bool:
key = get_key_of_obj(payload.data) if payload.key is None else payload.key
return await check_if_key_exist(key)
and the following test in my test file :
client = TestClient(app)
class ServiceTest(unittest.TestCase):
.....
def test_check_if_object_is_exist(self):
webrecord_json = {'a':1}
response = client.get("/webrecord/check_if_object_exist", json=webrecord_json)
assert response.status_code == 200
assert response.json(), "webrecord should already be in db, expected : True, got : {}".format(response.json())
When I run the code in debug I realized that the break points inside the get method aren't reached. When I changed the type of the request to post everything worked fine.
What am I doing wrong?
In order to send data to the server via a GET request, you'll have to encode it in the url, as GET does not have any body. This is not advisable if you need a particular format (e.g. JSON), since you'll have to parse the url, decode the parameters and convert them into JSON.
Alternatively, you may POST a search request to your server. A POST request allows a body which may be of different formats (including JSON).
If you still want GET request
#app.get('/webrecord/check_if_object_exist/{key}')
async def check_if_object_exist(key: str, data: str) -> bool:
key = get_key_of_obj(payload.data) if payload.key is None else payload.key
return await check_if_key_exist(key)
client = TestClient(app)
class ServiceTest(unittest.TestCase):
.....
def test_check_if_object_is_exist(self):
response = client.get("/webrecord/check_if_object_exist/key", params={"data": "my_data")
assert response.status_code == 200
assert response.json(), "webrecord should already be in db, expected : True, got : {}".format(response.json())
This will allow to GET requests from url mydomain.com/webrecord/check_if_object_exist/{the key of the object}.
One final note: I made all the parameters mandatory. You may change them by declaring to be None by default. See fastapi Docs
Related
I'm trying to work with a third party API and I am having problems with sending the request when using the requests or even urllib.request.
Somehow when I use http.client I am successful sending and receiving the response I need.
To make life easier for me, I created an API class below:
class API:
def get_response_data(self, response: http.client.HTTPResponse) -> dict:
"""Get the response data."""
response_body = response.read()
response_data = json.loads(response_body.decode("utf-8"))
return response_data
The way I use it is like this:
api = API()
rest_api_host = "api.app.com"
connection = http.client.HTTPSConnection(rest_api_host)
token = "my_third_party_token"
data = {
"token":token
}
payload = json.loads(data)
headers = {
# some headers
}
connection.request("POST", "/some/endpoint/", payload, headers)
response = connection.getresponse()
response_data = api.get_response_data(response) # I get a dictionary response
This workflow works for me. Now I just want to write a test for the get_response_data method.
How do I instantiate a http.client.HTTPResponse with the desired output to be tested?
For example:
from . import API
from unittest import TestCase
class APITestCase(TestCase):
"""API test case."""
def setUp(self) -> None:
super().setUp()
api = API()
def test_get_response_data_returns_expected_response_data(self) -> None:
"""get_response_data() method returns expected response data in http.client.HTTPResponse"""
expected_response_data = {"token": "a_secret_token"}
# I want to do something like this
response = http.client.HTTPResponse(expected_response_data)
self.assertEqual(api.get_response_data(response), expected_response_data)
How can I do this?
From the http.client docs it says:
class http.client.HTTPResponse(sock, debuglevel=0, method=None, url=None)
Class whose instances are returned upon successful connection. Not instantiated directly by user.
I tried looking at socket for the sock argument in the instantiation but honestly, I don't understand it.
I tried reading the docs in
https://docs.python.org/3/library/http.client.html#http.client.HTTPResponse
https://docs.python.org/3/library/socket.html
Searched the internet on "how to test http.client.HTTPResponse" but I haven't found the answer I was looking for.
I am trying to parse "#" symbol as a direct url in Flask project. The issue is everytime the url is requested, it breaks any value that has # init as it's a special character in url encoding.
localhost:9999/match/keys?source=#123&destination=#123
In flask, I am trying to get these arguments like this
app.route(f'/match/keys/source=<string:start>/destination=<string:end>', methods=['GET'])
The url response that i see on console is this:
"GET /match/keys/source=' HTTP/1.0" 404 -] happens
I believe you might not fully understand how 'query strings' work in flask. This url:
app.route(f'/match/keys/source=<string:start>/destination=<string:end>', methods=['GET'])
won't work as you expect as it won't match the request:
localhost:9999/match/keys?source=#123&destination=#123
rather it aught to be:
#app.route('/match/keys', methods=['GET'])
and this would match:
localhost:9999/match/keys?source=%23123&destination=%23123
Then to catch those 'query strings' you do:
source = request.args.get('source') # <- name the variable what you may
destination = request.args.get('destination') # <- same as the naming format above
So when you call localhost:9999/match/keys?source=%23123&destination=%23123 you test for those 'query strings' in the request url and if they are they that route function would execute.
I wrote this test:
def test_query_string(self):
with app.test_client() as c:
rc = c.get('/match/keys?source=%23123') # <- Note use of the '%23' to represent '#'
print('Status code: {}'.format(rc.status_code))
print(rc.data)
assert rc.status_code == 200
assert 'source' in request.args
assert rc.data.decode('utf-8') == "#123"
and it passes using this route function:
#app.route('/match/keys', methods=['GET'])
def some_route():
s = request.args.get('source')
return s
So you see I was able to catch the query string source value in my unit test.
I found another trick to work around with it. Instead of using GET method, I switched to POST
localhost:9999/match/keys
and in the app.routes, i sent the argument to get_json.
app.route('/match/keys/',method=['POST'])
def my_func():
arg = request.get_json
In postman, I send the POST request and send the body to be like this:
Postman Post request
Using the below route definition, I am trying to extract the book_id out of the URL in aiohttp.
from aiohttp import web
routes = web.RouteTableDef()
#routes.get('/books/{book_id}')
async def get_book_pages(request: web.Request) -> web.Response:
book_id = request.match_info.get('book_id', None)
return web.json_response({'book_id': book_id})
Below is the test (using pytest) I have written
import asynctest
import pytest
import json
async def test_get_book() -> None:
request = make_mocked_request('GET', '/books/1')
response = await get_book(request)
assert 200 == response.status
body = json.loads(response.body)
assert 1 == body['book_id']
Test Result:
None != 1
Expected :1
Actual :None
Outside of the tests, when I run a request to /books/1 the response is {'book_id': 1}
What is the correct way to retrieve dynamic values from the path in aiohttp when mocking the request?
make_mocked_request() knows nothing about an application and its routes.
To pass dynamic info you need to provide a custom match_info object:
async def test_get_book() -> None:
request = make_mocked_request('GET', '/books/1',
match_info={'book_id': '1'})
response = await get_book(request)
assert 200 == response.status
body = json.loads(response.body)
assert 1 == body['book_id']
P.S.
In general, I want to warn about mocks over-usage. Usually, functional testing with aiohttp_client is easier to read and maintain.
I prefer mocking for really hard-to-rest things like network errors emulation.
Otherwise your tests do test your own mocks, not a real code.
I have a Flask application that returns both HTML pages and JSON responses to API requests. I want to change what an error handler returns based on the content type of the request. If the client requests application/json, I want to return a jsonify response, otherwise I want to return a render_template response. How can I detect what was requested and change the response appropriately?
The current error handlers I have only return an HTML response.
def register_errorhandlers(app):
"""Register error handlers."""
def render_error(error):
"""Render error template."""
# If a HTTPException, pull the `code` attribute; default to 500
error_code = getattr(error, 'code', 500)
return render_template('{0}.html'.format(error_code)), error_code
for errcode in [401, 404, 500]:
app.errorhandler(errcode)(render_error)
Use request.content_type to get the content type the client sent with the request. Use request.accept_mimetypes the get the mimetypes the client indicated it can accept in a response. Use these to determine what to return.
from flask import request, jsonify, render_template
if request.accept_mimetypes.accept_json:
return jsonify(...)
else:
return render_template(...)
I used the after_request decorator to do this and checked the content type:
#app.after_request
def after_request_helper(resp):
if resp.content_type == "text/html":
# If a HTTPException, pull the `code` attribute; default to 500
error_code = getattr(error, 'code', 500)
return render_template('{0}.html'.format(error_code)), error_code
else:
return app.errorhandler(errcode)(render_error)
A more detailed answer:
def wants_json_response():
return request.accept_mimetypes['application/json'] >= \
request.accept_mimetypes['text/html']
The wants_json_response() helper function compares the preference for JSON or HTML selected by the client in their list of preferred formats. If JSON rates higher than HTML, then it is necessary to return a JSON response.
Otherwise, return the original HTML responses based on templates.
For the JSON responses would slightly supplement the function with one condition:
if wants_json_response(): which is what you need. So the answer is in that.
If the condition is true we could write a function that would generate a response:
def api_error_response(status_code, message=None):
payload = {'error': HTTP_STATUS_CODES.get(status_code, 'Unknown error')}
if message:
payload['message'] = message
response = jsonify(payload)
response.status_code = status_code
return response
This function uses the handy HTTP_STATUS_CODES dictionary from Werkzeug (a core dependency of Flask) that provides a short descriptive name for each HTTP status code.
For easier and faster understanding, 'error' is used to represent errors, so you only need to worry about the numeric status code and the optional long description.
The jsonify() function returns a Flask Response object with a default status code of 200, so after the response is created, it is necessary to set the status code to the correct one for the error.
So if we put it all together now it would look like this:
# app/__init__.py
import requests
def register_errorhandlers(app):
from .errors import render_error
for e in [
requests.codes.INTERNAL_SERVER_ERROR,
requests.codes.NOT_FOUND,
requests.codes.UNAUTHORIZED,
]:
app.errorhandler(e)(render_error)
and
# app/errors.py
import requests
from flask import render_template, request, jsonify
from werkzeug.http import HTTP_STATUS_CODES
from .extensions import db
def api_error_response(status_code, message=None):
payload = {'error': HTTP_STATUS_CODES.get(status_code, 'Unknown error')}
if message:
payload['message'] = message
response = jsonify(payload)
response.status_code = status_code
return response
def wants_json_response():
return request.accept_mimetypes['application/json'] >= \
request.accept_mimetypes['text/html']
def render_error(e):
if requests.codes.INTERNAL_SERVER_ERROR == e.code:
db.session.rollback()
if wants_json_response():
return api_error_response(e.code)
else:
return render_template(f'{e.code}.html'), e.code
Additionally
Then they could use the response generation for other cases as well.
The most common error that the API is going to return is going to be
the code 400, which is the error for “bad request”. This is the error
that is used when the client sends a request that has invalid data in it.
In order to generate messages to the function below even easier in these cases, we forward only the required description - message.
def bad_request(message):
return api_error_response(400, message)
I hope this will help in approaching with errors :)
According to official doc, you can make batch request through Google API Client Libraries for Python. Here, there is an example
from apiclient.http import BatchHttpRequest
def list_animals(request_id, response, exception):
if exception is not None:
# Do something with the exception
pass
else:
# Do something with the response
pass
def list_farmers(request_id, response):
"""Do something with the farmers list response."""
pass
service = build('farm', 'v2')
batch = service.new_batch_http_request()
batch.add(service.animals().list(), callback=list_animals)
batch.add(service.farmers().list(), callback=list_farmers)
batch.execute(http=http)
Is there a way to access to the request in the callback. E.g., print the request (not the request_id) if there is an exception?
I ran into this same issue, as I needed the original request for using compute.instances().aggregatedList_next() function.
I ended up passing a unique request_id to batch.add() and created a dictionary to keep track of the original requests that were made. Then within the callback, I referenced that dictionary and pulled out the request by using the unique request_id.
So I did something along the following:
requests = {}
project = 'example-project'
batch = service.new_batch_http_request(callback=callback_callable)
new_request = compute.instances().aggregatedList(project=project)
requests[project] = new_request
batch.add(new_request, request_id=project)
Within the callable function:
def callback_callable(self, request_id, response, exception):
original_request = requests[request_id]
Note that the value passed to the request_id needs to be a string. If an int is passed it will raise a TypeError quote_from_bytes() expected bytes.
Hope this helps someone!
In case it helps anyone - I'm using google-api-python-client==2.65.0
and the call back is passed differently - it is specified like this:
def list_animals(request_id, response, exception=None):
if exception is None:
original_request = requests[request_id]
# process here...
batch = service.new_batch_http_request(callback=list_animals)
batch.add(service.animals().list())
batch.add(service.farmers().list())
batch.execute()