Passed
Pull Request — master (#460)
by Aldo
05:49
created

kytos.core.rest_api.StarletteOpenAPIRequest.__init__()   A

Complexity

Conditions 1

Size

Total Lines 12
Code Lines 7

Duplication

Lines 12
Ratio 100 %

Importance

Changes 0
Metric Value
cc 1
eloc 7
nop 3
dl 12
loc 12
rs 10
c 0
b 0
f 0
1
"""Rest API utilities module."""
2
# pylint: disable=self-assigning-variable
3
import asyncio
4
import json
5
from asyncio import AbstractEventLoop
6
from datetime import datetime
7
from typing import Any, Optional
8
9
from starlette.exceptions import HTTPException
10
from starlette.requests import Request
11
from starlette.responses import JSONResponse as StarletteJSONResponse
12
13
14
def _json_serializer(obj):
15
    if isinstance(obj, datetime):
16
        return obj.isoformat()
17
    if isinstance(obj, set):
18
        return sorted(obj)
19
    raise TypeError(f"Type {type(obj)} not serializable")
20
21
22
def get_body(
23
    request: Request, loop: AbstractEventLoop, timeout: Optional[float] = None
24
) -> bytes:
25
    """Try to get request.body form a sync @rest route."""
26
    future = asyncio.run_coroutine_threadsafe(request.body(), loop)
27
    return future.result(timeout)
28
29
30
def get_json(
31
    request: Request, loop: AbstractEventLoop, timeout: Optional[float] = None
32
) -> Any:
33
    """Try to get request.json from a sync @rest route.
34
    It might raise json.decoder.JSONDecodeError.
35
    """
36
    future = asyncio.run_coroutine_threadsafe(request.json(), loop)
37
    return future.result(timeout)
38
39
40
def get_json_or_400(
41
    request: Request, loop: AbstractEventLoop, timeout: Optional[float] = None
42
) -> Any:
43
    """Try to get request.json from a sync @rest route or HTTPException 400."""
44
    try:
45
        return get_json(request, loop, timeout)
46
    except (json.decoder.JSONDecodeError, TypeError) as exc:
47
        raise HTTPException(400, detail=f"Invalid json: {str(exc)}")
48
49
50
async def aget_json_or_400(request: Request) -> Any:
51
    """Try to get request.json from async @rest route or HTTPException 400."""
52
    try:
53
        return await request.json()
54
    except (json.decoder.JSONDecodeError, TypeError) as exc:
55
        raise HTTPException(400, detail=f"Invalid json: {str(exc)}")
56
57
58
def content_type_json_or_415(request: Request) -> Optional[str]:
59
    """Ensures request Content-Type is application/json or raises 415."""
60
    content_type = request.headers.get("Content-Type", "")
61
    content_types = content_type.split(";")
62
    if "application/json" not in content_types:
63
        err = "Expected Content-Type: application/json, " \
64
              f"got: {content_type}"
65
        raise HTTPException(415, detail=err)
66
    return content_type
67
68
69
def error_msg(error_list: list) -> str:
70
    """Return a more request friendly error message from ValidationError"""
71
    msg = ""
72
    for err in error_list:
73
        for value in err['loc']:
74
            msg += str(value) + ", "
75
        msg = msg[:-2]
76
        msg += ": " + err["msg"] + "; "
77
    return msg[:-2]
78
79
80
class JSONResponse(StarletteJSONResponse):
81
    """JSONResponse with custom default serializer that supports datetime."""
82
83
    media_type = "application/json"
84
85
    def render(self, content) -> bytes:
86
        return json.dumps(
87
            content,
88
            ensure_ascii=False,
89
            allow_nan=False,
90
            indent=None,
91
            separators=(",", ":"),
92
            default=_json_serializer,
93
        ).encode("utf-8")
94