I am Test Developer and I'm trying to create basic HTTP Server mock app which could generate endpoints with using one "master endpoint" eg. /generate_endpoint.
I would provide url and body (and maybe response code later) to /generate_endpoint, and when I call endpoint which I created it will give me the "body" response.
It must work without restarting server, since I would like to use it multiple times with different urls and body's.
Below is code I tried for that.
If that isn't possible to dynamically create endpoints, then maybe you could give me advice - because I want to create Mock to test MyApp and the basic workflow is like that:
Check if order exists (MyApp)
MyApp connects to externalApi and checks if order exists (That i want to mock)
MyApp returns value based on what is given in externalApi
but there is multiple responses (and multiple endpoints) which might occur and I want to have test cases for them all so I will not need external app for my tests.
here is what I tried:
from fastapi import HTTPException
router = APIRouter()
endpoints = {}
def generate_route(url: str, body: dict):
async def route():
return body
router.get(path=url)(route)
endpoints[url] = body
#router.post("/generate_endpoint")
async def generate_endpoint(endpoint_data: dict):
endpoint_url = endpoint_data.get("url")
endpoint_body = endpoint_data.get("body")
if not endpoint_url or not endpoint_body:
raise HTTPException(status_code=400, detail="url and body required")
generate_route(endpoint_url, endpoint_body)
return {"message": f"route added for url {endpoint_url}"}
or
from flask_restful import Api, Resource, reqparse
app = Flask(__name__)
api = Api(app)
class GenerateEndpoint(Resource):
def post(self):
parser = reqparse.RequestParser()
parser.add_argument("url", type=str)
parser.add_argument("response", type=str)
args = parser.parse_args()
def response():
return args["response"]
api.add_resource(response, args["url"])
return {"status": "success", "url": args["url"]}, 201
api.add_resource(GenerateEndpoint, "/generate_endpoints")
if __name__ == "__main__":
app.run(debug=True)
then im testing generate_endpoints with something like {"url": "/someurl", "body": "something"}
and then i Expect when i call GET 127.0.0.1:5000/someurl i will have "something" response
You can use this variant:
import uvicorn
from fastapi import FastAPI, APIRouter, HTTPException
router = APIRouter()
endpoints = {}
#router.get("/{url:path}")
def get_response(url: str):
if url in endpoints:
return endpoints[url]
raise HTTPException(status_code=404, detail="Not Found")
def generate_route(url: str, body: dict):
endpoints[url] = body
#router.post("/generate_endpoint")
async def generate_endpoint(endpoint_data: dict):
endpoint_url = endpoint_data.get("url")
endpoint_body = endpoint_data.get("body")
if not endpoint_url or not endpoint_body:
raise HTTPException(status_code=400, detail="url and body required")
generate_route(endpoint_url, endpoint_body)
return {"message": f"route added for url {endpoint_url}"}
app = FastAPI()
app.include_router(router)
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port=5000)
Related
(I did find the following question on SO, but it didn't help me: Is it possible to have an api call another api, having them both in same application?)
I am making an app using Fastapi with the following folder structure
main.py is the entry point to the app
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.api.v1 import lines, upload
from app.core.config import settings
app = FastAPI(
title=settings.PROJECT_NAME,
version=0.1,
openapi_url=f'{settings.API_V1_STR}/openapi.json',
root_path=settings.ROOT_PATH
)
app.add_middleware(
CORSMiddleware,
allow_origins=settings.BACKEND_CORS_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(upload.router, prefix=settings.API_V1_STR)
app.include_router(lines.router, prefix=settings.API_V1_STR)
In the lines.py, I have 2 GET endpoints:
/one-random-line --> returns a random line from a .txt file
/one-random-line-backwards --> should return the output of the /one-random-line
Since the output of the second GET endpoint should be the reversed string of the output of the first GET endpoint, I tried doing the following steps mentioned here
The codes:
import random
from fastapi import APIRouter, Request
from starlette.responses import RedirectResponse
router = APIRouter(
prefix="/get-info",
tags=["Get Information"],
responses={
200: {'description': 'Success'},
400: {'description': 'Bad Request'},
403: {'description': 'Forbidden'},
500: {'description': 'Internal Server Error'}
}
)
#router.get('/one-random-line')
def get_one_random_line(request: Request):
lines = open('netflix_list.txt').read().splitlines()
if request.headers.get('accept') in ['application/json', 'application/xml']:
random_line = random.choice(lines)
else:
random_line = 'This is an example'
return {'line': random_line}
#router.get('/one-random-line-backwards')
def get_one_random_line_backwards():
url = router.url_path_for('get_one_random_line')
response = RedirectResponse(url=url)
return {'message': response[::-1]}
When I do this, I get the following error:
TypeError: 'RedirectResponse' object is not subscriptable
When I change the return of the second GET endpoint to return {'message': response}, I get the following output
What is the mistake I am doing?
Example:
If the output of /one-random-line endpoint is 'Maverick', then the output of /one-random-line-backwards should be 'kcirevam'
You can just call any endpoint from your code directly as a function call, you don't have to deal with RedirectResponse() or anything. Below is an example of how this would look like and will run as-is:
from fastapi import FastAPI, Request
app = FastAPI()
#app.get("/one-random-line")
async def get_one_random_line(request: Request):
# implement your own logic here, this will only return a static line
return {"line": "This is an example"}
#app.get("/one-random-line-backwards")
async def get_one_random_line_backwards(request: Request):
# You don't have to do fancy http stuff, just call your endpoint:
one_line = await get_one_random_line(request)
return {"line": one_line["line"][::-1]}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
Using curl we get the following result:
% curl localhost:8000/one-random-line
{"line":"This is an example"}%
% curl localhost:8000/one-random-line-backwards
{"line":"elpmaxe na si sihT"}%
Refactor your code to have the common part as a function you call - you'd usually have this in a module external to your controller.
# this function could live as LineService.get_random_line for example
# its responsibility is to fetch a random line from a file
def get_random_line(path="netflix_list.txt"):
lines = open(path).read().splitlines()
return random.choice(lines)
# this function encodes the rule that "if the accepted response is json or xml
# we do the random value, otherwise we return a default value"
def get_random_or_default_line_for_accept_value(accept, path="netflix_list.txt", default_value="This is an example"):
if accept not in ("application/json", "application/xml"):
return default_value
return get_random_line(path=path)
#router.get('/one-random-line')
def get_one_random_line(request: Request):
return {
"line": get_random_or_default_line_for_accept_value(
accept=request.headers.get('accept'),
),
}
#router.get('/one-random-line-backwards')
def get_one_random_line_backwards(request: Request):
return {
"line": get_random_or_default_line_for_accept_value(
accept=request.headers.get('accept'),
)[::-1],
}
I have a python client application that calls (http) a python Flask api.
Both of these applications are logging to Azure Application insights using the opencensus libraries.
I want to do the logging in a fashion so that I can correlate the logs in ApplicationInsights end to end.
Python client app
For example, when the client app initiates an HTTP GET call to the Flask API, it generates an http request dependency log entry in ApplicationInsights.
The app also logs individual entries about the http request and http response into the trace table.
Flask API
I am logging the incoming HTTP request in the Flask API using request decorator, and also logging the HTTP response using a request decorator.
Also the actual method ( that the Flask routing invokes ) has its own logging.
Note These are logs go into trace table.
Expectation
I am trying to get the logs generated from the Flask API have a correlation with the log generated from the client application.
Current behaviour
Logs of Python client app
The logs in the dependency table have a operation_Id - All good!
The logs in the trace table have the same operation_Id and operation_ParentId as above - All good!
Logs of Flask api
The logs in the request table have the same operation_Id as above - All good!
The logs in the trace table generated by the before_request, after_request decorators - The operation_Id and operation_ParentId are blank. - Problematic!
The logs in the trace table generated by the logging statements inside the route/methods - The operation_Id and operation_ParentId are blank. - Problematic!
Help please
I can see that Traceparent http header is coming in as part of the http request in the Flask API, but looks like logging is ignoring this.
How do I get the logging statements to use the Traceparent data so that operation_Id and operation_ParentId show up correctly in the traces table for the Flask API?
Flask API Code
import flask
from flask import request, jsonify
import logging
import json
import requests
from opencensus.ext.azure.log_exporter import AzureLogHandler,AzureEventHandler
from opencensus.ext.flask.flask_middleware import FlaskMiddleware
from opencensus.ext.azure.trace_exporter import AzureExporter
from opencensus.trace.samplers import ProbabilitySampler, AlwaysOnSampler
from opencensus.trace.tracer import Tracer
from opencensus.trace import config_integration
import os
logger = logging.getLogger()
class MyJSONEncoder(flask.json.JSONEncoder):
def default(self, obj):
if isinstance(obj, decimal.Decimal):
# Convert decimal instances to strings.
return str(obj)
if isinstance(obj, datetime.datetime):
return obj.strftime(strftime_iso_regular_format_str)
return super(MyJSONEncoder, self).default(obj)
# Initialize logging with Azure Application Insights
class CustomDimensionsFilter(logging.Filter):
"""Add custom-dimensions like run_id in each log by using filters."""
def __init__(self, custom_dimensions=None):
"""Initialize CustomDimensionsFilter."""
self.custom_dimensions = custom_dimensions or {}
def filter(self, record):
"""Add the default custom_dimensions into the current log record."""
dim = {**self.custom_dimensions, **
getattr(record, "custom_dimensions", {})}
record.custom_dimensions = dim
return True
APPLICATION_INSIGHTS_CONNECTIONSTRING=os.getenv('APPLICATION_INSIGHTS_CONNECTIONSTRING')
modulename='FlaskAPI'
APPLICATION_NAME='FlaskAPI'
ENVIRONMENT='Development'
def callback_function(envelope):
envelope.tags['ai.cloud.role'] = APPLICATION_NAME
return True
logger = logging.getLogger(__name__)
log_handler = AzureLogHandler(
connection_string=APPLICATION_INSIGHTS_CONNECTIONSTRING)
log_handler.addFilter(CustomDimensionsFilter(
{
'ApplicationName': APPLICATION_NAME,
'Environment': ENVIRONMENT
}))
log_handler.add_telemetry_processor(callback_function)
logger.addHandler(log_handler)
azureExporter = AzureExporter(
connection_string=APPLICATION_INSIGHTS_CONNECTIONSTRING)
azureExporter.add_telemetry_processor(callback_function)
tracer = Tracer(exporter=azureExporter, sampler=AlwaysOnSampler())
app = flask.Flask("app")
app.json_encoder = MyJSONEncoder
app.config["DEBUG"] = True
middleware = FlaskMiddleware(
app,
exporter=azureExporter,
sampler=ProbabilitySampler(rate=1.0),
)
config_integration.trace_integrations(['logging', 'requests'])
def getJsonFromRequestBody(request):
isContentTypeJson = request.headers.get('Content-Type') == 'application/json'
doesHaveBodyJson = False
if isContentTypeJson:
try:
doesHaveBodyJson = request.get_json() != None
except:
doesHaveBodyJson = False
if doesHaveBodyJson == True:
return json.dumps(request.get_json())
else:
return None
def get_properties_for_customDimensions_from_request(request):
values = ''
if len(request.values) == 0:
values += '(None)'
for key in request.values:
values += key + ': ' + request.values[key] + ', '
properties = {'custom_dimensions':
{
'request_method': request.method,
'request_url': request.url,
'values': values,
'body': getJsonFromRequestBody(request)
}}
return properties
def get_properties_for_customDimensions_from_response(request,response):
request_properties = request_properties = get_properties_for_customDimensions_from_request(request)
request_customDimensions = request_properties.get('custom_dimensions')
response_properties = {'custom_dimensions':
{
**request_customDimensions,
'response_status':response.status,
'response_body':response.data.decode('utf-8')
}
}
return response_properties
# Useful debugging interceptor to log all values posted to the endpoint
#app.before_request
def before():
properties = get_properties_for_customDimensions_from_request(request)
logger.warning("request {} {}".format(
request.method, request.url), extra=properties)
# Useful debugging interceptor to log all endpoint responses
#app.after_request
def after(response):
response_properties = get_properties_for_customDimensions_from_response(request,response)
logger.warning("response: {}".format(
response.status
),extra=response_properties)
return response
#app.route('/api/{}/status'.format("v1"), methods=['GET'])
def health_check():
message = "Health ok!"
logger.info(message)
return message
if __name__ == '__main__':
app.run()
References used
Microsoft's guidance on Application Insights Log Correlation
My code repository where I have tested and reproduced the problem
Say you are using fastapi.testclient.TestClient to perform a GET, for instance. Inside the API code that defines that GET method, if you get request.client.host, you will get the string "testclient".
Test, using pytest:
def test_success(self):
client = TestClient(app)
client.get('/my_ip')
Now, lets assume your API code is something like this:
#router.get('/my_ip')
def my_ip(request: Request):
return request.client.host
The endpoit /my_ip is suppose to return the client IP, but when running pytest, it will return "testclient" string. Is there a way to change the client IP (host) on TestClient to something other than "testclient"?
You can mock the fastapi.Request.client property as,
# main.py
from fastapi import FastAPI, Request
app = FastAPI()
#app.get("/")
def root(request: Request):
return {"host": request.client.host}
# test_main.py
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_read_main(mocker):
mock_client = mocker.patch("fastapi.Request.client")
mock_client.host = "192.168.123.132"
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"host": "192.168.123.132"}
I have a simple FastAPI setup as below,
# main.py
from fastapi import FastAPI
app = FastAPI()
#app.on_event("shutdown")
def app_shutdown():
with open("shutdown-test-file.txt", "w") as fp:
fp.write("FastAPI app has been terminated")
#app.get("/")
def root():
return {"message": "Hello World"}
How can I write (unit)test for this app_shutdown(...) functionality?
Related Posts
This SO post is also asking similar question, but, not in a "testing context"
The official doc has something similar, but, there is no example for on_event("shutdown")
According to the documentation, you need to wrap it in the context manager (a with statement) to trigger the events, something like this:
def test_read_items():
with TestClient(app) as client:
response = client.get("/items/foo")
assert response.status_code == 200
If you use pytest, you can set up a fixture for it like this:
from main import app
from fastapi.testclient import TestClient
import pytest
#pytest.fixture
def client():
with TestClient(app) as c:
yield c
def test_read_main(client):
response = client.get("/")
assert response.status_code == 200
I'm quite new to the FastAPI framework, I want to restrict my request header content type with "application/vnd.api+json", But I can't able to find a way to configure my content type with the Fast API route instance.
Any info will be really useful.
A better approach is to declare dependency:
from fastapi import FastAPI, HTTPException, status, Header, Depends
app = FastAPI()
def application_vnd(content_type: str = Header(...)):
"""Require request MIME-type to be application/vnd.api+json"""
if content_type != "application/vnd.api+json":
raise HTTPException(
status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
f"Unsupported media type: {content_type}."
" It must be application/vnd.api+json",
)
#app.post("/some-path", dependencies=[Depends(application_vnd)])
def some_path(q: str = None):
return {"result": "All is OK!", "q": q}
So it can be reused if needed.
For successful request it'll return something like this:
{
"result": "All is OK!",
"q": "Some query"
}
And for unsuccessful something like this:
{
"detail": "Unsupported media type: type/unknown-type. It must be application/vnd.api+json"
}
Each request has the content-type in its headers. You could check it like so:
import uvicorn
from fastapi import FastAPI, HTTPException
from starlette import status
from starlette.requests import Request
app = FastAPI()
#app.get("/hello")
async def hello(request: Request):
content_type = request.headers.get("content-type", None)
if content_type != "application/vnd.api+json":
raise HTTPException(
status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
detail=f"Unsupported media type {content_type}")
return {"content-type": content_type}
if __name__ == '__main__':
uvicorn.run("main", host="127.0.0.1", port=8080)
Hope that helps 🙂