Skip to content

Commit

Permalink
add path parameter decorator
Browse files Browse the repository at this point in the history
  • Loading branch information
Tuerke Erik committed Mar 11, 2020
1 parent 46af9a8 commit 99ccb22
Show file tree
Hide file tree
Showing 8 changed files with 104 additions and 22 deletions.
2 changes: 2 additions & 0 deletions restit/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from .development_server import DevelopmentServer
from .hyperlink import Hyperlink
from .namespace import Namespace
from .path_parameter_decorator import path_parameter
from .query_parameters_decorator import query_parameters
from .request import Request
from .request_body_decorator import request_body
from .request_mapping import request_mapping
from .resource import Resource
from .response import Response
from .restit_app import RestitApp
from .restit_test_app import RestitTestApp
Expand Down
2 changes: 1 addition & 1 deletion restit/hyperlink.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import Type

from restit import Request
from restit.internal.resource_path import ResourcePath
from restit.request import Request
from restit.resource import Resource


Expand Down
12 changes: 12 additions & 0 deletions restit/path_parameter_decorator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from typing import Type

from restit.resource import Resource, PathParameter


# noinspection PyShadowingBuiltins
def path_parameter(name: str, type=str, doc: str = None):
def decorator(clazz: Type[Resource]):
clazz.add_path_parameter(PathParameter(name, type, doc))
return clazz

return decorator
37 changes: 35 additions & 2 deletions restit/resource.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
from collections import namedtuple, defaultdict
from typing import Tuple, AnyStr, Dict, Union

from werkzeug import Request
from werkzeug.exceptions import MethodNotAllowed
from werkzeug.exceptions import MethodNotAllowed, BadRequest

from restit.internal.resource_path import ResourcePath
from restit.response import Response

PathParameter = namedtuple("PathParameter", ["name", "type", "doc"])

_PATH_PARAMETER_MAPPING = defaultdict(list)


class Resource:
__request_mapping__ = None

def __init__(self):
self._resource_path = None

@classmethod
def add_path_parameter(cls, path_parameter: PathParameter):
_PATH_PARAMETER_MAPPING[cls].append(path_parameter)

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

Expand Down Expand Up @@ -53,8 +62,32 @@ def head(self, request: Request) -> Response:

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

passed_path_parameters = self._collect_and_convert_path_parameters(path_params)

return method(request, **passed_path_parameters)

def _collect_and_convert_path_parameters(self, path_params: dict):
for path_parameter in _PATH_PARAMETER_MAPPING[self.__class__]:
try:
path_parameter_value = path_params[path_parameter.name]
except KeyError:
raise Resource.PathParameterNotFoundException(
f"Unable to find {path_parameter} in incoming path parameters {path_params}"
)
try:
path_params[path_parameter.name] = path_parameter.type(path_parameter_value)
except ValueError as error:
raise BadRequest(
f"Path parameter value '{path_parameter_value}' is not matching '{path_parameter}' "
f"({str(error)})"
)

return path_params

def _get_match(self, url: str) -> Tuple[bool, Union[None, Dict[str, AnyStr]]]:
assert self._resource_path
return self._resource_path.get_match(url)

class PathParameterNotFoundException(Exception):
pass
8 changes: 4 additions & 4 deletions restit/restit_test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ def __init__(self, restit_app: RestitApp):
)
self._init()


def get(self, path: str, json: dict = None, data: dict = None, headers: dict = None) -> Response:
return self._get_response_for_method(path, json, data, headers, "GET")

Expand All @@ -47,7 +46,7 @@ def _get_response_for_method(self, path: str, json: dict, data: dict, headers: d

def _get_response(self, wsgi_environment):
request = Request(wsgi_environment)
resource, path_params = self._find_resource_for_url(request.get_path())
resource, path_params = self._find_resource_for_url(request.get_extended_request_info().path)
if self._raise_exceptions:
return self._get_response_or_raise_not_found(path_params, request, resource)
else:
Expand All @@ -58,6 +57,7 @@ def _create_wsgi_environment(
):
header = header or {}
wsgi_environment = {}
parsed_path = urlparse(path)
setup_testing_defaults(wsgi_environment)
body_as_bytes = b""
if json is not None:
Expand All @@ -66,10 +66,10 @@ def _create_wsgi_environment(
body_as_bytes = "&".join([f"{key}={value}" for key, value in data.items()])
body_as_bytes = body_as_bytes.encode(encoding=get_default_encoding())
wsgi_environment["REQUEST_METHOD"] = method
wsgi_environment["PATH_INFO"] = path
wsgi_environment["PATH_INFO"] = parsed_path.path
wsgi_environment["CONTENT_LENGTH"] = len(body_as_bytes)
wsgi_environment["wsgi.input"] = BytesIO(body_as_bytes)
wsgi_environment["QUERY_STRING"] = urlparse(path).query
wsgi_environment["QUERY_STRING"] = parsed_path.query
wsgi_environment["HTTP_ACCEPT"] = header.get("Accept", "*/*")
wsgi_environment["HTTP_ACCEPT_ENCODING"] = header.get("Accept-Encoding", "gzip, deflate")
wsgi_environment["CONTENT_TYPE"] = header.get("Content-Type", self._get_content_type(json, data))
Expand Down
36 changes: 36 additions & 0 deletions test/restit/path_parameter_decorator_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import unittest

from restit import request_mapping, Request, Response, path_parameter, RestitApp, RestitTestApp
from restit.resource import Resource


@request_mapping("/path/:id1/and/:id2/and/:id3")
@path_parameter("id1", type=int, doc="First path parameter")
@path_parameter("id2", type=float, doc="Second path parameter")
@path_parameter("id3")
class Resource1(Resource):
def get(self, request: Request, **path_params) -> Response:
return Response(path_params)


class PathParameterTestCase(unittest.TestCase):
def setUp(self) -> None:
restit_app = RestitApp(resources=[
Resource1()
])
self.restit_test_app = RestitTestApp(restit_app)

def test_path_parameters(self):
response = self.restit_test_app.get("/path/1/and/10/and/20")
self.assertEqual(200, response.get_status_code())
self.assertEqual({'id1': 1, 'id2': 10.0, 'id3': '20'}, response.json())

def test_conversion_exception(self):
response = self.restit_test_app.get("/path/1/and/hans/and/20")
self.assertEqual(400, response.get_status_code())
self.assertEqual(
"<title>400 Bad Request</title>\n"
"<h1>Bad Request</h1>\n"
"<p>Path parameter value 'hans' is not matching 'PathParameter(name='id2', type=<class 'float'>, "
"doc='Second path parameter')' (could not convert string to float: 'hans')</p>\n", response.text
)
28 changes: 14 additions & 14 deletions test/restit/query_parameter_decorator_test.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import requests
import unittest

from marshmallow import Schema, fields
from werkzeug import Request
from werkzeug.exceptions import BadRequest

from restit import RestitApp, RestitTestApp
from restit.query_parameters_decorator import query_parameters
from restit.request_mapping import request_mapping
from restit.resource import Resource
from restit.response import Response
from test.base_test_server_test_case import BaseTestServerTestCase


class QueryParameterSchema(Schema):
Expand All @@ -29,23 +30,22 @@ def get(self, request: Request) -> Response:
return Response(request.query_parameters)


class QueryParameterDecoratorTestCase(BaseTestServerTestCase):
@classmethod
def setUpClass(cls) -> None:
BaseTestServerTestCase.resources = [
class QueryParameterDecoratorTestCase(unittest.TestCase):
def setUp(self) -> None:
restit_app = RestitApp(resources=[
QueryParametersResource(),
CustomErrorClassResource()
]
BaseTestServerTestCase.setUpClass()
])
self.restit_test_app = RestitTestApp(restit_app)

def test_query_parameter(self):
response = requests.get(f"http://127.0.0.1:{self.port}/queryparams?param1=1&param2=huhu")
self.assertEqual(200, response.status_code)
response = self.restit_test_app.get("/queryparams?param1=1&param2=huhu")
self.assertEqual(200, response.get_status_code())
self.assertEqual({'param1': 1, 'param2': 'huhu'}, response.json())

def test_validation_error_gives_422_status(self):
response = requests.get(f"http://127.0.0.1:{self.port}/queryparams?param1=1&")
self.assertEqual(422, response.status_code)
response = self.restit_test_app.get(f"/queryparams?param1=1&")
self.assertEqual(422, response.get_status_code())
self.assertEqual(
"<title>422 Unprocessable Entity</title>\n"
"<h1>Unprocessable Entity</h1>\n"
Expand All @@ -54,8 +54,8 @@ def test_validation_error_gives_422_status(self):
)

def test_validation_error_custom_error_class(self):
response = requests.get(f"http://127.0.0.1:{self.port}/custom-error-class?param1=1&")
self.assertEqual(400, response.status_code)
response = self.restit_test_app.get(f"/custom-error-class?param1=1&")
self.assertEqual(400, response.get_status_code())
self.assertEqual(
"<title>400 Bad Request</title>\n"
"<h1>Bad Request</h1>\n"
Expand Down
1 change: 0 additions & 1 deletion test/restit/test_restit_test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ class NoMethodsResource(Resource):

@request_mapping("/miau/:id")
class ResourceWithPathParams(Resource):

def get(self, request: Request, **path_params) -> Response:
return Response(path_params)

Expand Down

0 comments on commit 99ccb22

Please sign in to comment.