diff --git a/docs/basics.md b/docs/basics.md index 02a0864..1c71f65 100644 --- a/docs/basics.md +++ b/docs/basics.md @@ -184,7 +184,7 @@ def route_handler(res, req): ## Using ujson, orjson or any custom JSON serializer -socketify by default uses built in `json` module with have great performance on PyPy, but if you wanna to use another module instead of the default you can just register using `app.json_serializer(module)` +socketify by default uses built-in `json` module with has great performance on PyPy, but if you wanna use another module instead of the default you can just register using `app.json_serializer(module)` ```python from socketify import App diff --git a/docs/middlewares.md b/docs/middlewares.md index 740ca83..99aa1a7 100644 --- a/docs/middlewares.md +++ b/docs/middlewares.md @@ -57,6 +57,13 @@ auth_router.get("/another", middleware(another_middie, home)) other_router = MiddlewareRouter(app, auth, another_middie) other_router.get("/another_way", home) +# you can also use middlewares when using the decorator router +private = app.router("/api", auth, another_middie) + +# will serve in /api/users and use auth_middleware +@private.get("/users") +def get_users(res, req, user): + res.cork_end("Hello private API!") app.listen( 3000, diff --git a/docs/routes.md b/docs/routes.md index 0f0521d..69ca7ce 100644 --- a/docs/routes.md +++ b/docs/routes.md @@ -24,6 +24,34 @@ app.post("/", home) ``` > Whenever your callback is a coroutine, such as the async/await, automatic corking can only happen in the very first portion of the coroutine (consider await a separator which essentially cuts the coroutine into smaller segments). Only the first "segment" of the coroutine will be called from socketify, the following async segments will be called by the asyncio event loop at a later point in time and will thus not be under our control with default corking enabled, HttpRequest object being stack-allocated and only valid in one single callback invocation so only valid in the first "segment" before the first await. If you just want to preserve headers, url, method, cookies and query string you can use `req.preserve()` to copy all data and keep it in the request object, but will be some performance penalty. Take a look in [Corking](corking.md) for get a more in deph information + +You can also use the `Decorator router` as the name suggests this router allows to use of decorators for routing and also comes up with a prefix option, and middleware support. + +```python +from socketify import App + +app = App() +router = app.router() + +@router.get("/") +def home(res, req): + res.end("Hello World!") + +api = app.router(prefix="/api") + +# will serve in /api/hello +@api.get("/hello") +def hello(res, req): + res.end("Hello API!") + +private = app.router("/api", auth_middleware) + +# will serve in /api/users and use auth_middleware +@private.get("/users") +def get_users(res, req, auth): + res.end("Hello private API!") +``` + ## Pattern matching Routes are matched in order of specificity, not by the order you register them: diff --git a/src/socketify/helpers.py b/src/socketify/helpers.py index ec35f10..0110b63 100644 --- a/src/socketify/helpers.py +++ b/src/socketify/helpers.py @@ -123,8 +123,69 @@ def static_route(app, route, directory): route = route[:-1] app.get("%s/*" % route, route_handler) - def middleware(*functions): + syncs = [] + asyncs = [] + for function in functions: + # all is async after the first async + if inspect.iscoroutinefunction(function) or len(asyncs) > 0: + asyncs.append(function) + else: + syncs.append(function) + if len(asyncs) == 0: # pure sync + return sync_middleware(*functions) + if len(syncs) == 0: # pure async + return async_middleware(*functions) + + # we use Optional data=None at the end so you can use and middleware inside a middleware + def optimized_middleware_route(res, req, data=None): + # cicle to all middlewares + for function in syncs: + # call middlewares + data = function(res, req, data) + # stops if returns Falsy + if not data: + return + async def wrapper(res, req, data): + # cicle to all middlewares + for function in asyncs: + # detect if is coroutine or not + if inspect.iscoroutinefunction(function): + data = await function(res, req, data) + else: + # call sync middleware + data = function(res, req, data) + # stops if returns Falsy + if not data: + break + return data + + # in async query string, arguments and headers are only valid until the first await + # preserve queries, headers, parameters, url, full_url and method + req.preserve() + + # go async + res.run_async(wrapper(res, req, data)) + + return optimized_middleware_route + + + +def sync_middleware(*functions): + # we use Optional data=None at the end so you can use and middleware inside a middleware + def middleware_route(res, req, data=None): + # cicle to all middlewares + for function in functions: + # call middlewares + data = function(res, req, data) + # stops if returns Falsy + if not data: + break + return data + + return middleware_route + +def async_middleware(*functions): # we use Optional data=None at the end so you can use and middleware inside a middleware async def middleware_route(res, req, data=None): some_async_as_run = False @@ -149,67 +210,195 @@ def middleware(*functions): return middleware_route +class DecoratorRouter: + def __init__(self, app, prefix: str="", *middlewares): + self.app = app + self.middlewares = middlewares + self.prefix = prefix + + def get(self, path): + path = f"{self.prefix}{path}" + + def decorator(handler): + if len(self.middlewares) > 0: + middies = list(*self.middlewares) + middies.append(handler) + self.app.get(path, middleware(*middies)) + else: + self.app.get(path, handler) + return handler + + return decorator + + def post(self, path): + path = f"{self.prefix}{path}" + def decorator(handler): + if len(self.middlewares) > 0: + middies = list(*self.middlewares) + middies.append(handler) + self.app.post(path, middleware(*middies)) + else: + self.app.post(path, handler) + + return decorator + + def options(self, path): + path = f"{self.prefix}{path}" + def decorator(handler): + if len(self.middlewares) > 0: + middies = list(*self.middlewares) + middies.append(handler) + self.app.options(path, middleware(*middies)) + else: + self.app.options(path, handler) + + return decorator + + def delete(self, path): + path = f"{self.prefix}{path}" + def decorator(handler): + if len(self.middlewares) > 0: + middies = list(*self.middlewares) + middies.append(handler) + self.app.delete(path, middleware(*middies)) + else: + self.app.delete(path, handler) + + return decorator + + def patch(self, path): + path = f"{self.prefix}{path}" + def decorator(handler): + if len(self.middlewares) > 0: + middies = list(*self.middlewares) + middies.append(handler) + self.app.patch(path, middleware(*middies)) + else: + self.app.patch(path, handler) + + return decorator + + def put(self, path: str): + path = f"{self.prefix}{path}" + def decorator(handler): + if len(self.middlewares) > 0: + middies = list(*self.middlewares) + middies.append(handler) + self.app.put(path, middleware(*middies)) + else: + self.app.put(path, handler) + + return decorator + + def head(self, path): + path = f"{self.prefix}{path}" + def decorator(handler): + if len(self.middlewares) > 0: + middies = list(*self.middlewares) + middies.append(handler) + self.app.head(path, middleware(*middies)) + else: + self.app.head(path, handler) + + return decorator + + def connect(self, path): + path = f"{self.prefix}{path}" + def decorator(handler): + if len(self.middlewares) > 0: + middies = list(*self.middlewares) + middies.append(handler) + self.app.connect(path, middleware(*middies)) + else: + self.app.connect(path, handler) + + return decorator + + def trace(self, path): + path = f"{self.prefix}{path}" + def decorator(handler): + if len(self.middlewares) > 0: + middies = list(*self.middlewares) + middies.append(handler) + self.app.trace(path, middleware(*middies)) + else: + self.app.trace(path, handler) + + return decorator + + def any(self, path): + path = f"{self.prefix}{path}" + def decorator(handler): + if len(self.middlewares) > 0: + middies = list(*self.middlewares) + middies.append(handler) + self.app.any(path, middleware(*middies)) + else: + self.app.any(path, handler) + + return decorator + class MiddlewareRouter: def __init__(self, app, *middlewares): self.app = app self.middlewares = middlewares def get(self, path, handler): - middies = list(self.middlewares) + middies = list(*self.middlewares) middies.append(handler) self.app.get(path, middleware(*middies)) return self def post(self, path, handler): - middies = list(self.middlewares) + middies = list(*self.middlewares) middies.append(handler) self.app.post(path, middleware(*middies)) return self def options(self, path, handler): - middies = list(self.middlewares) + middies = list(*self.middlewares) middies.append(handler) self.app.options(path, middleware(*middies)) return self def delete(self, path, handler): - middies = list(self.middlewares) + middies = list(*self.middlewares) middies.append(handler) self.app.delete(path, middleware(*middies)) return self def patch(self, path, handler): - middies = list(self.middlewares) + middies = list(*self.middlewares) middies.append(handler) self.app.patch(path, middleware(*middies)) return self def put(self, path, handler): - middies = list(self.middlewares) + middies = list(*self.middlewares) middies.append(handler) self.app.put(path, middleware(*middies)) return self def head(self, path, handler): - middies = list(self.middlewares) + middies = list(*self.middlewares) middies.append(handler) self.app.head(path, middleware(*middies)) return self def connect(self, path, handler): - middies = list(self.middlewares) + middies = list(*self.middlewares) middies.append(handler) self.app.connect(path, middleware(*middies)) return self def trace(self, path, handler): - middies = list(self.middlewares) + middies = list(*self.middlewares) middies.append(handler) self.app.trace(path, middleware(*middies)) return self def any(self, path, handler): - middies = list(self.middlewares) + middies = list(*self.middlewares) middies.append(handler) self.app.any(path, middleware(*middies)) return self diff --git a/src/socketify/socketify.py b/src/socketify/socketify.py index 5742170..62306cf 100644 --- a/src/socketify/socketify.py +++ b/src/socketify/socketify.py @@ -17,6 +17,7 @@ from .loop import Loop from .status_codes import status_codes from .helpers import static_route from dataclasses import dataclass +from .helpers import DecoratorRouter mimetypes.init() @@ -2159,6 +2160,9 @@ class App: self._response_extension = None self._ws_extension = None + def router(self, prefix: str="", *middlewares): + return DecoratorRouter(self, prefix, middlewares) + def register(self, extension): if self._request_extension is None: self._request_extension = AppExtension() diff --git a/src/tests.py b/src/tests.py index 1a0ceb1..03901de 100644 --- a/src/tests.py +++ b/src/tests.py @@ -25,7 +25,17 @@ def extension(request, response, ws): # extensions must be registered before routes app.register(extension) -async def home(res, req): +def auth_middleware(res, req, data): + token = req.get_query("token") + print("token?", token) + req.token = token + return { "name": "Test" } if token else { "name", "Anonymous" } + +router = app.router("", auth_middleware) + +@router.get("/") +async def home(res, req, data=None): + print(data) print("token", req.token) cart = await req.get_cart() print("cart", cart) @@ -34,7 +44,7 @@ async def home(res, req): print("token", req.token) res.send("Hello World!") -app.get("/", home) + app.listen( 3000, lambda config: print("Listening on port http://localhost:%d now\n" % config.port),