Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Framework inside web request handler #2

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .vscode/requests.http
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,24 @@ POST http://localhost:5000/bound
Content-Type: application/json
X-Request-Id: bar

{
"institution_id": ["1001"],
"filters": {
"account_id": "123456"
}
}

###
GET http://localhost:5050/foo

###
POST http://localhost:5050/broken

###
POST http://localhost:5050/bound
Content-Type: application/json
X-Request-Id: bar

{
"institution_id": ["1001"],
"filters": {
Expand Down
16 changes: 13 additions & 3 deletions abstractions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,27 @@

from dataclasses import dataclass, field

from tornado.httputil import HTTPServerRequest
from tornado.httputil import HTTPHeaders


@dataclass
class HTTPRequest:
url: str
path: str
query: str
method: str = 'GET'
headers: HTTPHeaders = field(default_factory=HTTPHeaders)
body: bytes = b''


@dataclass
class HTTPResponse:
status_code: int = 500
headers: dict = field(default_factory=dict)
headers: HTTPHeaders = field(default_factory=HTTPHeaders)
body: bytes = b''
error: Exception = None


AppDelegate = typing.Callable[[HTTPServerRequest], typing.Coroutine[typing.Any, typing.Any, HTTPResponse]]
AppDelegate = typing.Callable[[HTTPRequest], typing.Coroutine[typing.Any, typing.Any, HTTPResponse]]

Middleware = typing.Callable[[AppDelegate], AppDelegate]
107 changes: 66 additions & 41 deletions core.py
Original file line number Diff line number Diff line change
@@ -1,59 +1,84 @@
import asyncio
import collections
from typing import Sequence
import typing

import tornado.httputil
import tornado.routing
import tornado.web

from abstractions import AppDelegate, Middleware
import abstractions
from middleware.exception import convert_exception_to_response


class PipelineRouter(tornado.routing.ReversibleRuleRouter):
def __init__(self, rules=None, middleware: Sequence[Middleware] = None):
super().__init__(rules)
self.middleware = middleware if isinstance(middleware, collections.Sequence) else []
def wrap_middleware(delegate: abstractions.AppDelegate,
middlewares: typing.Sequence[abstractions.Middleware]) -> abstractions.AppDelegate:
result = convert_exception_to_response(delegate)

def get_target_delegate(self, target, request, **target_params):
target_kwargs = target_params.get('target_kwargs')
return PipelineDelegate(request, middleware=self.middleware, **target_kwargs)
for middleware in reversed(middlewares):
result = convert_exception_to_response(middleware(result))

return result

class PipelineDelegate(tornado.httputil.HTTPMessageDelegate):
request_handler: AppDelegate

def __init__(self, request: tornado.httputil.HTTPServerRequest, delegate: AppDelegate,
middleware: Sequence[Middleware], *args, **kwargs):
self.request = request
class BaseHandler(tornado.web.RequestHandler):
delegate: abstractions.AppDelegate
request_handler: abstractions.AppDelegate

def initialize(self, **kwargs):
delegate = kwargs.pop('delegate')
middleware = self.settings.get('middleware')

if not callable(delegate):
raise ValueError('delegate must be callable')

self.delegate = delegate
self.middleware = middleware
self._chunks = []
self.request_handler = wrap_middleware(delegate, middleware) if isinstance(middleware, collections.Sequence) else delegate

def _make_request(self) -> abstractions.HTTPRequest:
return abstractions.HTTPRequest(
url=self.request.uri,
path=self.request.path,
query=self.request.query,
method=self.request.method,
headers=dict(self.request.headers),
body=self.request.body
)

def _write_response(self, response: abstractions.HTTPResponse):
self.set_status(response.status_code)

for k, v in response.headers.items():
self.set_header(k, v)

self.finish(response.body)

async def _process_request(self, *args, **kwargs):
response = await self.request_handler(self._make_request())

self._write_response(response)

async def get(self, *args, **kwargs):
await self._process_request(*args, **kwargs)

async def post(self, *args, **kwargs):
await self._process_request(*args, **kwargs)


self.load_middleware()
Route = typing.Tuple[str, abstractions.AppDelegate]

def load_middleware(self):
self.request_handler = convert_exception_to_response(self.delegate)

for middleware in reversed(self.middleware):
handler = middleware(self.request_handler)
self.request_handler = convert_exception_to_response(handler)
class TornadoService:
routes: typing.List[Route]

def data_received(self, chunk):
self._chunks.append(chunk)
def __init__(self, routes: typing.List[Route], middlewares: typing.Sequence[abstractions.Middleware] = None):
if not isinstance(routes, (list, tuple)):
raise ValueError('routes')

def finish(self):
self.request.body = b''.join(self._chunks)
self.request._parse_body()
asyncio.create_task(self.handle_request())
self.routes = routes
self.middlewares = middlewares

def on_connection_close(self):
self._chunks = None
def make_application(self, **settings) -> tornado.web.Application:
handlers = [(path, BaseHandler, dict(delegate=delegate), next(iter(name), None)) for path, delegate, *name in
self.routes]
return tornado.web.Application(handlers, **{'middleware': self.middlewares, **settings})

async def handle_request(self):
response = await self.request_handler(self.request)
reason = tornado.httputil.responses.get(response.status_code, 'Unknown')
await self.request.connection.write_headers(
tornado.httputil.ResponseStartLine('', response.status_code, reason),
tornado.httputil.HTTPHeaders(response.headers),
response.body)
self.request.connection.finish()
def run(self, port, host: str = "", **settings):
app = self.make_application(**settings)
app.listen(port, host)
4 changes: 2 additions & 2 deletions example/handlers/bound.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from marshmallow import Schema, fields

from abstractions import HTTPResponse
from abstractions import HTTPResponse, HTTPHeaders
from handlers.bind_request import bind_arguments, Json, Header


Expand All @@ -25,6 +25,6 @@ async def bound(request, data: RequestSchema, js: Json, message_id: Header('X-Re

return HTTPResponse(
status_code=200,
headers={'Contetnt-Type': 'application/json', 'Foo': message_id},
headers=HTTPHeaders({'Content-Type': 'application/json', 'Foo': message_id}),
body=json.dumps(data).encode()
)
9 changes: 5 additions & 4 deletions example/handlers/dependency.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import tornado.httputil
import urllib.parse

from abstractions import HTTPResponse
from abstractions import HTTPResponse, HTTPRequest


class HandlerWithDependency:
def __init__(self, greeting: str):
self.greeting = greeting

async def __call__(self, request: tornado.httputil.HTTPServerRequest) -> HTTPResponse:
name = request.query_arguments.get('name', [b''])[0].decode()
async def __call__(self, request: HTTPRequest) -> HTTPResponse:
query = urllib.parse.parse_qs(request.query)
name = query.get('name')[0]
return HTTPResponse(
status_code=200,
body='{greeting}, {name}'.format(greeting=self.greeting, name=name).encode()
Expand Down
4 changes: 2 additions & 2 deletions example/handlers/wrapped.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from abstractions import HTTPResponse
from abstractions import HTTPResponse, HTTPHeaders
from middleware.request_id import request_id_middleware


@request_id_middleware
async def wrapped(request):
return HTTPResponse(
status_code=200,
headers={'Content-Type': 'application/json', 'Foo': 'bar'},
headers=HTTPHeaders({'Content-Type': 'application/json', 'Foo': 'bar'}),
body=b'{"foo": "bar"}')
19 changes: 10 additions & 9 deletions example/main.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import asyncio

from tornado.httpserver import HTTPServer

from core import PipelineRouter, PipelineDelegate
from core import TornadoService
from example.handlers.bound import bound
from example.handlers.dependency import HandlerWithDependency
from example.handlers.throwing import throw
from example.handlers.wrapped import wrapped
from middleware import request_id

if __name__ == "__main__":
HTTPServer(PipelineRouter([
('/hello', PipelineDelegate, {'delegate': HandlerWithDependency('hello')}),
('/bound', PipelineDelegate, {'delegate': bound}),
('/throw', PipelineDelegate, {'delegate': throw}),
('.*', PipelineDelegate, {'delegate': wrapped})
])).listen(5000)
routes = [
('/hello', HandlerWithDependency('hello')),
('/bound', bound),
('/throw', throw),
('.*', wrapped)
]

TornadoService(routes, [request_id.request_id_middleware]).run(5000)

loop = asyncio.get_event_loop()
loop.run_forever()
17 changes: 7 additions & 10 deletions example/routing_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,8 @@
import json
import logging

import tornado.httputil
from tornado.httpserver import HTTPServer

from abstractions import HTTPResponse, AppDelegate
from core import PipelineRouter
from abstractions import HTTPResponse, AppDelegate, HTTPRequest, HTTPHeaders
from core import TornadoService
from handlers.bind_request import Json, Header, bind_arguments
from middleware.request_id import request_id_middleware
from routing import route
Expand All @@ -34,19 +31,19 @@ async def bound(request, data: Json, message_id: Header('X-Request-Id')) -> HTTP
data.update({'message_id': message_id})
return HTTPResponse(
status_code=200,
headers={'Content-Type': 'application/json'},
headers=HTTPHeaders({'Content-Type': 'application/json'}),
body=json.dumps(data).encode()
)


def logging_middleware(func: AppDelegate) -> AppDelegate:
@functools.wraps(func)
async def wrapper(request: tornado.httputil.HTTPServerRequest) -> HTTPResponse:
async def wrapper(request: HTTPRequest) -> HTTPResponse:
response = await func(request)

logging.debug('{method} {path} {status} {error}'.format(
method=request.method.upper(),
path=request.uri,
path=request.url,
status=response.status_code,
error=response.error
))
Expand All @@ -57,10 +54,10 @@ async def wrapper(request: tornado.httputil.HTTPServerRequest) -> HTTPResponse:


if __name__ == '__main__':
HTTPServer(PipelineRouter(
TornadoService(
route.get_routes(),
[logging_middleware, request_id_middleware]
)).listen(5050)
).run(5050)

loop = asyncio.get_event_loop()
loop.run_forever()
17 changes: 8 additions & 9 deletions handlers/bind_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
from typing import get_type_hints, Any, MutableSet

from marshmallow.schema import SchemaMeta
from tornado.httputil import HTTPServerRequest

from abstractions import HTTPResponse, AppDelegate
from abstractions import HTTPRequest, HTTPResponse, AppDelegate


class RequestArgumentResolver(metaclass=ABCMeta):
Expand All @@ -16,7 +15,7 @@ def can_resolve_type(self, arg_type: type) -> bool:
pass

@abstractmethod
def resolve(self, request: HTTPServerRequest, arg_name: str, arg_type: type) -> Any:
def resolve(self, request: HTTPRequest, arg_name: str, arg_type: type) -> Any:
pass


Expand All @@ -42,7 +41,7 @@ class SchemaResolver(RequestArgumentResolver):
def can_resolve_type(self, arg_type: type):
return isinstance(arg_type, SchemaMeta)

def resolve(self, request: HTTPServerRequest, arg_name: str, arg_type: type):
def resolve(self, request: HTTPRequest, arg_name: str, arg_type: type):
result, _ = arg_type(strict=True).loads(request.body)
return result

Expand All @@ -53,8 +52,8 @@ class JsonResolver(RequestArgumentResolver):
def can_resolve_type(self, arg_type: type):
return arg_type is Json

def resolve(self, request: HTTPServerRequest, arg_name: str, arg_type: type):
if request.headers['Content-Type'] == 'application/json' and request.method.upper() in self.ALLOWED_METHODS:
def resolve(self, request: HTTPRequest, arg_name: str, arg_type: type):
if request.headers.get('Content-Type') == 'application/json' and request.method.upper() in self.ALLOWED_METHODS:
return json.loads(request.body)

raise ArgumentResolveError(arg_type)
Expand All @@ -64,7 +63,7 @@ class HeaderResolver(RequestArgumentResolver):
def can_resolve_type(self, arg_type: type):
return isinstance(arg_type, Header)

def resolve(self, request: HTTPServerRequest, arg_name: str, arg_type: Header):
def resolve(self, request: HTTPRequest, arg_name: str, arg_type: Header):
return request.headers.get(arg_type.name, None)


Expand All @@ -77,7 +76,7 @@ def __str__(self):
return "Unexpected argument type: {arg_type}".format(arg_type=self.arg_type)


def _resolve_argument_value(request: HTTPServerRequest, arg_name, arg_type):
def _resolve_argument_value(request: HTTPRequest, arg_name, arg_type):
for resolver in ARGUMENT_RESOLVERS:
if resolver.can_resolve_type(arg_type):
return resolver.resolve(request, arg_name, arg_type)
Expand All @@ -92,7 +91,7 @@ def bind_arguments(func) -> AppDelegate:
:return: An `AppDelegate` deriving inner handler arguments from type hints and `HTTPServerRequest`.
"""
@functools.wraps(func)
async def wrapper(request: HTTPServerRequest) -> HTTPResponse:
async def wrapper(request: HTTPRequest) -> HTTPResponse:
annotations = get_type_hints(func)
annotations.pop('return', None)

Expand Down
Loading