Skip to content

Commit

Permalink
use werkzeug Request class
Browse files Browse the repository at this point in the history
  • Loading branch information
Tuerke Erik committed Mar 10, 2020
1 parent 374cf99 commit 70222f1
Show file tree
Hide file tree
Showing 18 changed files with 300 additions and 228 deletions.
3 changes: 2 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,11 @@ Quick example
if __name__ == "__main__":
# start a development server on http://127.0.0.1:5000
app.start_development_server()
You can also use a production-ready server like `Gunicorn <https://gunicorn.org/>`_)
You can also use a production-ready server like `Gunicorn <https://gunicorn.org/>`_
(given the name of the above module is `restit_app_test.py`):

.. code-block:: bash
Expand Down
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
marshmallow
marshmallow
werkzeug
7 changes: 6 additions & 1 deletion restit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
import logging
import sys

DEFAULT_ENCODING = sys.getdefaultencoding()
_DEFAULT_ENCODING = sys.getdefaultencoding()

LOGGER = logging.getLogger(__name__)
LOGGER.setLevel(level=logging.WARNING)


def set_default_encoding(default_encoding: str):
global _DEFAULT_ENCODING
_DEFAULT_ENCODING = default_encoding
17 changes: 0 additions & 17 deletions restit/_internal/common.py

This file was deleted.

46 changes: 46 additions & 0 deletions restit/_internal/exception_response_maker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from werkzeug.datastructures import MIMEAccept
from werkzeug.exceptions import HTTPException

from restit.response import Response


class ExceptionResponseMaker:
HTML_TEMPLATE = """<title>{code} {name}</title>
<h1>{name}</h1>
<p>{description}</p>
"""

def __init__(self, http_exception: HTTPException):
self.http_exception = http_exception

def create_response(self, accept: MIMEAccept) -> Response:
if accept.accept_html:
return self.create_html_response()
elif accept.accept_json:
return self.create_json_response()
else:
return self.create_plain_text_response()

def create_html_response(self) -> Response:
html_text = ExceptionResponseMaker.HTML_TEMPLATE.format(
code=self.http_exception.code,
name=self.http_exception.name,
description=self.http_exception.description
)
response = Response(html_text, status_code=self.http_exception.code)
return response

def create_json_response(self) -> Response:
response_dict = {
"code": self.http_exception.code,
"name": self.http_exception.name,
"description": self.http_exception.description
}
response = Response(response_dict, self.http_exception.code)
return response

def create_plain_text_response(self) -> Response:
return Response(
f"{self.http_exception.name}: {self.http_exception.description} ({self.http_exception.code})",
self.http_exception.code
)
52 changes: 0 additions & 52 deletions restit/_internal/wsgi_request_environment.py

This file was deleted.

15 changes: 4 additions & 11 deletions restit/expect_query_parameters_decorator.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,15 @@
from http import HTTPStatus

from marshmallow import Schema, ValidationError

from restit.request import Request
from restit.response import Response
from werkzeug import Request
from werkzeug.exceptions import UnprocessableEntity


def expect_query_parameters(schema: Schema, validation_error_status=HTTPStatus.UNPROCESSABLE_ENTITY):
def expect_query_parameters(schema: Schema, validation_error_class=UnprocessableEntity):
def decorator(func):
def wrapper(self, request: Request, **path_parameters):
try:
request.query_parameters = schema.load(request.query_parameters)
except ValidationError as error:
return Response.from_http_status(
http_status=validation_error_status,
description="Query parameter validation failed",
additional_description=str(error)
)
raise validation_error_class(f"Query parameter validation failed ({str(error)})")
else:
return func(self, request, **path_parameters)

Expand Down
14 changes: 4 additions & 10 deletions restit/expect_request_body_decorator.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,16 @@
from http import HTTPStatus

from marshmallow import Schema, ValidationError
from werkzeug.exceptions import UnprocessableEntity

from restit.request import Request
from restit.response import Response


def expect_request_body(schema: Schema, validation_error_status=HTTPStatus.UNPROCESSABLE_ENTITY):
def expect_request_body(schema: Schema, validation_error_class=UnprocessableEntity):
def decorator(func):
def wrapper(self, request: Request, **path_parameters):
try:
request.body_as_json = schema.load(request.body_as_json)
request.body_as_dict = schema.load(request.body_as_dict)
except ValidationError as error:
return Response.from_http_status(
http_status=validation_error_status,
description="Request body validation failed",
additional_description=str(error)
)
raise validation_error_class(f"Request body validation failed ({str(error)})")
else:
return func(self, request, **path_parameters)

Expand Down
62 changes: 42 additions & 20 deletions restit/request.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,43 @@
import json
from json import JSONDecodeError

from restit import DEFAULT_ENCODING
from restit._internal.common import create_dict_from_query_parameter_syntax


class Request:
def __init__(self, query_parameters: dict, wsgi_environment: dict = None, body: bytes = None):
self.query_parameters = query_parameters
self.wsgi_environment = wsgi_environment
self.body: bytes = body or b""
self.body_as_json: dict = {}
self.__set_body_as_json()

def __set_body_as_json(self):
body_as_string = self.body.decode(encoding=DEFAULT_ENCODING)
try:
self.body_as_json = json.loads(body_as_string)
except JSONDecodeError:
self.body_as_json = create_dict_from_query_parameter_syntax(body_as_string)
from functools import lru_cache

import werkzeug.wrappers
from werkzeug.utils import escape

from restit import _DEFAULT_ENCODING


class Request(werkzeug.wrappers.Request):

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

self.query_parameters = {}
self.body_as_dict = {}
self._init()

def _init(self):
self.query_parameters = \
self._create_dict_from_assignment_syntax(self.query_string.decode(encoding=_DEFAULT_ENCODING))
if self.is_json():
self.body_as_dict.update(json.loads(self.data.decode(encoding=_DEFAULT_ENCODING)))
else:
self.body_as_dict.update(dict(self.form))

def is_json(self) -> bool:
return self.content_type.lower() == "application/json"

@staticmethod
@lru_cache()
def _create_dict_from_assignment_syntax(assignment_syntax_data: str) -> dict:
return_dict = {}
if assignment_syntax_data is None or "=" not in assignment_syntax_data:
return return_dict
for query_string_pair in assignment_syntax_data.split("&"):
try:
key, value = query_string_pair.split("=")
return_dict[key] = escape(value)
except ValueError:
pass

return return_dict
40 changes: 15 additions & 25 deletions restit/resource.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from http import HTTPStatus
from typing import Tuple, AnyStr, Dict, Union

from werkzeug import Request
from werkzeug.exceptions import MethodNotAllowed

from restit._internal.resource_path import ResourcePath
from restit._internal.wsgi_request_environment import RequestType
from restit.request import Request
from restit.response import Response


Expand All @@ -12,58 +12,48 @@ class Resource:

def __init__(self):
self._resource_path = None
self.__request_type_mapping = {
RequestType.GET: self.get,
RequestType.PUT: self.put,
RequestType.POST: self.post,
RequestType.DELETE: self.delete,
RequestType.OPTIONS: self.options,
RequestType.PATCH: self.patch,
RequestType.CONNECT: self.connect,
RequestType.HEAD: self.head,
RequestType.TRACE: self.trace
}

def init(self):
self._resource_path = ResourcePath(self.__request_mapping__)

def get(self, request: Request) -> Response:
return Response.from_http_status(HTTPStatus.METHOD_NOT_ALLOWED)
raise MethodNotAllowed()

# noinspection PyMethodMayBeStatic,PyUnusedLocal
def post(self, request: Request) -> Response:
return Response.from_http_status(HTTPStatus.METHOD_NOT_ALLOWED)
raise MethodNotAllowed()

# noinspection PyMethodMayBeStatic,PyUnusedLocal
def put(self, request: Request) -> Response:
return Response.from_http_status(HTTPStatus.METHOD_NOT_ALLOWED)
raise MethodNotAllowed()

# noinspection PyMethodMayBeStatic,PyUnusedLocal
def delete(self, request: Request) -> Response:
return Response.from_http_status(HTTPStatus.METHOD_NOT_ALLOWED)
raise MethodNotAllowed()

# noinspection PyMethodMayBeStatic,PyUnusedLocal
def patch(self, request: Request) -> Response:
return Response.from_http_status(HTTPStatus.METHOD_NOT_ALLOWED)
raise MethodNotAllowed()

# noinspection PyMethodMayBeStatic,PyUnusedLocal
def options(self, request: Request) -> Response:
return Response.from_http_status(HTTPStatus.METHOD_NOT_ALLOWED)
raise MethodNotAllowed()

# noinspection PyMethodMayBeStatic,PyUnusedLocal
def trace(self, request: Request) -> Response:
return Response.from_http_status(HTTPStatus.METHOD_NOT_ALLOWED)
raise MethodNotAllowed()

# noinspection PyMethodMayBeStatic,PyUnusedLocal
def connect(self, request: Request) -> Response:
return Response.from_http_status(HTTPStatus.METHOD_NOT_ALLOWED)
raise MethodNotAllowed()

# noinspection PyMethodMayBeStatic,PyUnusedLocal
def head(self, request: Request) -> Response:
return Response.from_http_status(HTTPStatus.METHOD_NOT_ALLOWED)
raise MethodNotAllowed()

def _handle_request(self, request_method: RequestType, request: Request, path_params: Dict) -> Response:
return self.__request_type_mapping[request_method](request, **path_params)
def _handle_request(self, request_method: str, request: Request, path_params: Dict) -> Response:
method = getattr(self, request_method.lower())
return method(request, **path_params)

def _get_match(self, url: str) -> Tuple[bool, Union[None, Dict[str, AnyStr]]]:
assert self._resource_path
Expand Down
4 changes: 2 additions & 2 deletions restit/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from http import HTTPStatus
from typing import Union

from restit import DEFAULT_ENCODING
from restit import _DEFAULT_ENCODING


class Response:
Expand All @@ -15,7 +15,7 @@ def __init__(
self.response_body = response_body
self.status_code = HTTPStatus(status_code, None)
self.header = header or {}
self.encoding = encoding or DEFAULT_ENCODING
self.encoding = encoding or _DEFAULT_ENCODING

@staticmethod
def from_http_status(
Expand Down

0 comments on commit 70222f1

Please sign in to comment.