Creating a base response for API calls - python

I want to create an API by using django-rest-framework. So far I've managed to setup one endpoint of API and managed to fetch all items. A basic response (without the BaseResponse class described later) would look like this:
[
{
"uuid": "1db6a08d-ec63-4beb-8b41-9b042c53ab83",
"created_at": "2018-03-12T19:25:07.073620Z",
"updated_at": "2018-03-12T19:25:37.904350Z",
"deleted_at": null,
"random_name": "random name"
}
]
The result I would like to achieve would be something like this:
[
"success": true
"message": "Some exception message",
"data" :{
"uuid": "1db6a08d-ec63-4beb-8b41-9b042c53ab83",
"created_at": "2018-03-12T19:25:07.073620Z",
"updated_at": "2018-03-12T19:25:37.904350Z",
"deleted_at": null,
"random_name": "random name"
}
]
I managed to achieve this by creating a BaseReponse class and in view I simply return BaseResponse.to_dict() (a method that I have created inside of class).
class BaseResponse(object):
data = None
success = False
message = None
def __init__(self, data, exception):
self.data = data
self.message = str(exception) if exception is not None else None
self.success = exception is None
def to_dict(self):
return {
'success': self.success,
'message': self.message,
'data': self.data,
}
View:
class RandomModelList(APIView):
def get(self, request, format=None):
exception = None
models = None
try:
models = RandomModel.objects.all()
except Exception as e:
exception = e
serializer = RandomModelSerializer(models, many=True)
base_response = BaseResponse(data=serializer.data, exception=exception)
return Response(base_response.to_dict())
I want to mention that with the current code everything its working as expected but I have a huge double about the code (I just feel like I reinvented the wheel). Can someone tell me if this is the optimal solution for my problem and if not what should I change/use?

You can create a Custom Renderer instead. Something like
class CustomRenderer(JSONRenderer):
def render(self, data, accepted_media_type=None, renderer_context=None):
resp = {
'data': data
}
return super(CustomRenderer, self).render(resp, accepted_media_type, renderer_context)
Then create a middleware like
class CustomResponseMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
response.data.update({'success': is_client_error(response.status_code) or is_server_error(response.status_code)})
return response

Related

Check Permissions in FastAPI + Stawberry GraphQL

I'm using BasePermission decorator as specified in documentation.
#strawberry.type
class Query:
#strawberry.field(permission_classes=[IsAuthenticated])
def user(self) -> User:
# get by token OFC
return User(user_id=1, email="vladimir#cw.tech", first_name = "Vladimir", last_name = "Kirilov")
In my impementation I use VerifyToken class as described in FastAPI auth0 documentation.
class IsAuthenticated(BasePermission):
message = "User is not authenticated"
def has_permission(self, source: Any, info: Info, **kwargs) -> bool:
print(source)
print(info)
token: str = Depends(token_auth_scheme)
print(token)
result = VerifyToken(token.credentials).verify()
if result.get("status"):
print(result)
return False
return True
So I'm trying to get and verify the BEARER from the request, but I'm not able to extract it to process it further and get the error, please advise.
{
"data": null,
"errors": [
{
"message": "'Depends' object has no attribute 'credentials'",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"user"
]
}
]
}
Figured it out, not the cleanest way, but here it is
class IsAuthenticated(BasePermission):
message = "User is not authenticated"
async def has_permission(self, source: Any, info: Info, **kwargs) -> bool:
request: Union[Request, WebSocket] = info.context["request"]
print(request.headers)
if "Authorization" in request.headers:
print(request.headers['Authorization'])
result = VerifyToken( request.headers['Authorization'][7:] ).verify()
if result.get("status") == "error":
print(result.get("msg"))
return False
if result.get("sub"):
print(result)
return True
return False

Incrementing self variable with decorator function

I want to increment an id counter for generating jsonrpc calls. I initially wanted to do this with a decorator function which can be annotated the requests methods I will write in the future.
To code:
def increment_id(func):
#functools.wraps(func)
def wrapper(*args, **kwargs):
# increment id of ServiceInterface.current_id
args[0].current_id += 1
return func(*args, **kwargs)
return wrapper
class ServiceInterface:
def __init__(
self, base_url,
):
self.base_url = base_url
self.headers = {"content-type": "application/json"}
self.current_id = 0
def send(self, payload):
"""
Returns json dict of JSONPRC request.
Parameters:
payload (JSONRPC dict): A dictionary in the form of a JSONRPC request.
Returns:
JSON dictionary response.
"""
response = requests.request(
"POST", self.base_url, data=payload, headers=self.headers
)
if response.ok:
return response.json()
else:
return {"error": {"code": response.status_code, "message": "HTTP Response Error"}}
#increment_id
def create_payload(self, method, params):
"""
Creates a JSONRPC request.
Parameters:
method (str): The RPC method to call.
params (dict): The arguments to the method call.
"""
payload = {
"jsonrpc": "2.0",
"method": method,
"id": self.current_id,
"params": params,
}
return json.dumps(payload)
From testing my code it seems like I can't access the variable current_id from args[0].
AttributeError: 'ServiceInterface' object has no attribute 'client_id'
Code for testing:
import unittest
URL = "http://seed-1.testnet.networks.dash.org:3000/"
# Valid method and parameters for said endpoint
METHOD = "getBlockHash"
PARAMS = {"height": 1}
class TestBase(unittest.TestCase):
def setUp(self):
self.interface = ServiceInterface(base_url=URL)
def test_create_payload(self):
VALID_RPC_PAYLOAD = json.dumps({
"jsonrpc": "2.0",
"method": "getBlockHash",
"id": 1,
"params": {"height": 1},
})
payload = self.interface.create_payload(METHOD, PARAMS)
# assert correct creation
self.assertEqual(payload, VALID_RPC_PAYLOAD)
# assert incrementation of id
payload_1 = json.loads(self.interface.create_payload(METHOD, PARAMS))
self.assertEqual(payload_1["id"], 2)
The foremost question is how do I access the self variable of the decorated item?
Secondly, is there a more elegant method of achieving this?

Is it possible to change the pydantic error messages in fastAPI?

In the FastAPI framework, the pydantic error messages are showing like below.
{"detail": [
{
"loc": [
"body",
"location",
"name"
],
"msg": "field required",
"type": "value_error.missing"
},
{
"loc": [
"body",
"location",
"name12"
],
"msg": "extra fields not permitted",
"type": "value_error.extra"
}
]
}
I want to send a simple message: {"field-name":"error message"}.
In Pydantic document they mentioned like, create a model instance in the try: except blocks and construct the error message in the except block. But in fast API, model instance created by fastapi itself, for example, if I write an URL like below
#router.post("/", response_model=DataModelOut)
async def create_location(location: schemas.LocationIn, user: str = Depends(get_current_user) ):
return model.save(location,user)
Here the location instance created by fastapi itself is the problem.
Is there any way to construct the error message?
Actually this error messages coming from fastapi.exceptions, You can achieve that by overriding the custom exceptions,
Imagine i have simple app like this:
from fastapi import Request, status
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from pydantic import BaseModel
#app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content=jsonable_encoder({"detail": exc.errors(),
"body": exc.body,
"your_additional_errors": {"Will be": "Inside", "This":" Error message"}}),
)
class Item(BaseModel):
title: str
size: int
#app.post("/items/")
async def create_item(item: Item):
return item
If i send values invalid values to my Request body
{
"title": 22,
"size": "hehe"
}
Now the error will be more customized:
{
"detail": [
{
"loc": [
"body",
"size"
],
"msg": "value is not a valid integer",
"type": "type_error.integer"
}
],
"body": {
"title": 22,
"size": "hehe"
},
"your_additional_errors": {
"Will be": "Inside the",
"Error": "Message"
}
}
You can change the content of exception, everything is up to you.
I am writing a middle ware for it.
async def genrange(s):
import json
s = json.loads(s)
yield json.dumps({"message":{k.get("loc")[-1]:k.get("msg") for k in s['detail']},
"id":None})
#app.middleware("http")
async def add_process_time_header(request: Request, call_next):
response = await call_next(request)
status_code = response.status_code
if status_code >=300:
async for i in response.body_iterator:
data = genrange(i)
response.body_iterator = data
return response
I'm not sure you'll like my answer better than the other answers. You can create a custom pydantic, with diffrent error messages. Kinda overkill, but could solve you a specific problem. In this example, I am changing the error message, when I insert an unpermitted HttpUrlSceme.
class UrlSchemePermittedError(errors.UrlError):
code = 'url.scheme'
msg_template = 'URL scheme not cool'
def __init__(self, allowed_schemes: Set[str]):
super().__init__(allowed_schemes=allowed_schemes)
class AnyHttpUrlDirrentMessage(AnyUrl):
allowed_schemes = {'http', 'https'}
#classmethod
def validate_parts(cls, parts: Dict[str, str]) -> Dict[str, str]:
"""
A method used to validate parts of an URL.
Could be overridden to set default values for parts if missing
"""
scheme = parts['scheme']
if scheme is None:
raise errors.UrlSchemeError()
if cls.allowed_schemes and scheme.lower() not in cls.allowed_schemes:
raise UrlSchemePermittedError(cls.allowed_schemes)
port = parts['port']
if port is not None and int(port) > 65_535:
raise errors.UrlPortError()
user = parts['user']
if cls.user_required and user is None:
raise errors.UrlUserInfoError()
return parts

DRF-Extension cache ignoring query parameters

I am using drf-extension for caching my APIs. But it is not working as expected with cache_response decorator.
It caches the response for say /api/get-cities/?country=india . But when I hit /api/get-cities/?country=usa, I get the same response.
Here is the sample code:
settings.py
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/0",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient"
},
"KEY_PREFIX": "city"
}
}
REST_FRAMEWORK_EXTENSIONS = {
'DEFAULT_USE_CACHE': 'default',
'DEFAULT_CACHE_RESPONSE_TIMEOUT': 86400,
}
views.py
class GetCities(APIView):
#cache_response()
def get(self, request):
country = request.GET.get("country", "")
return get_cities_function(country)
Please help with this.
I was able to find a solution for the problem. I created my own keys in redis with the combination of api name and parameter name(in my case country). So when API is hit with query parameters, I check if a key exists corresponding to that, if it exists then cached response is returned.
class GetCities(APIView):
def calculate_cache_key(self, view_instance, view_method, request, args, kwargs):
api = view_instance.get_view_name().replace(' ', '')
return "api:" + api + "country:" + str(request.GET.get("country", ""))
#cache_response(key_func='calculate_cache_key')
def get(self, request):
country = request.GET.get("country", "")
return get_cities_function(country)

Serializing nested object to string in Django Rest Framework

I am trying to connect my Django application to the mailchimps API with help from the Django Rest Framework, if I want to create a batch operation, I need to send the following call:
{
"operations": [
{
"method": "PUT",
"path": "lists/abc123/members/817f1571000f0b843c2b8a6982813db2",
"body": "{\"email_address\":\"hall#hallandoates.com\", \"status_if_new\":\"subscribed\"}"
},...
]
}
As you can see, the body object should be a json string. To create these calls, I created a model operation:
models.py
class Operation(object):
def __init__(self, method, list, contact):
email_clean = contact.email_address.lower().encode('utf-8')
subscriber_hash = hashlib.md5(email_clean).hexdigest()
serializer = ContactSerializer(contact)
body = serializer.data
self.method = method
self.path = "lists/%s/members/%s" % (list, subscriber_hash)
self.body = body
And the following serializer:
serializer.py
class OperationSerializer(serializers.Serializer):
method = serializers.CharField()
path = serializers.CharField()
body = serializers.CharField(allow_blank=True, required=False)
When I use my serializer to generate JSON, and parse the data with JSONRenderer(), the following call is returned:
{
"operations": [
{
"method": "PUT",
"path": "lists/abc123/members/817f1571000f0b843c2b8a6982813db2",
"body": "{\'email_address\':\'hall#hallandoates.com\', \'status_if_new\':\'subscribed\'}"
},...
]
}
This call breaks because of the single quotes, can anyone help me a hand in solving this?

Categories

Resources