diff --git a/README.md b/README.md index aa1fa084..e8ff751d 100644 --- a/README.md +++ b/README.md @@ -21,15 +21,15 @@ db.define_models(Task) app.pipeline = [db.pipe] def is_authenticated(): - return request.headers["Api-Key"] == "foobar" + return request.headers.get("api-key") == "foobar" def not_authorized(): response.status = 401 return {'error': 'not authorized'} @app.route(methods='get') -@service.json @requires(is_authenticated, otherwise=not_authorized) +@service.json async def todo(): page = request.query_params.page or 1 tasks = Task.where( diff --git a/emmett/__version__.py b/emmett/__version__.py index 667b52f9..43b48b6f 100644 --- a/emmett/__version__.py +++ b/emmett/__version__.py @@ -1 +1 @@ -__version__ = "2.5.2" +__version__ = "2.5.3" diff --git a/emmett/cli.py b/emmett/cli.py index ad912f58..2e9590ca 100644 --- a/emmett/cli.py +++ b/emmett/cli.py @@ -320,6 +320,9 @@ def develop_command( @click.option( '--loop', type=click.Choice(['auto', 'asyncio', 'uvloop']), default='auto', help='Event loop implementation.') +@click.option( + '--opt/--no-opt', is_flag=True, default=False, + help='Enable loop optimizations.') @click.option( '--log-level', type=click.Choice(LOG_LEVELS.keys()), default='info', help='Logging level.') @@ -332,8 +335,8 @@ def develop_command( '--ssl-keyfile', type=str, default=None, help='SSL key file') @pass_script_info def serve_command( - info, host, port, workers, threads, threading_mode, interface, ws, loop, log_level, - backlog, ssl_certfile, ssl_keyfile + info, host, port, workers, threads, threading_mode, interface, ws, loop, opt, + log_level, backlog, ssl_certfile, ssl_keyfile ): app_target = info._get_import_name() sgi_run( @@ -342,6 +345,7 @@ def serve_command( host=host, port=port, loop=loop, + loop_opt=opt, log_level=log_level, workers=workers, threads=threads, diff --git a/emmett/forms.py b/emmett/forms.py index fe13e067..e2b1678e 100644 --- a/emmett/forms.py +++ b/emmett/forms.py @@ -547,10 +547,10 @@ def selected(k): _id=_id or field.name ) - #: TO-DO - #@staticmethod - #def widget_list(attr, field, value, _class="", _id=None): - # return "" + @staticmethod + def widget_list(field, value): + options, _ = FormStyle._field_options(field) + return FormStyle.widget_multiple(None, field, value, options) @staticmethod def widget_upload(attr, field, value, _class="upload", _id=None): diff --git a/emmett/http.py b/emmett/http.py index ee21ab3a..d882b476 100644 --- a/emmett/http.py +++ b/emmett/http.py @@ -17,7 +17,7 @@ from email.utils import formatdate from hashlib import md5 -from typing import Any, BinaryIO, Dict, Generator, Tuple +from typing import Any, AsyncIterable, BinaryIO, Dict, Generator, Iterable, Tuple from granian.rsgi import HTTPProtocol @@ -284,7 +284,7 @@ async def asgi(self, scope, send): async def _send_body(self, send): more_body = True while more_body: - chunk = await self.io_stream.read(self.chunk_size) + chunk = self.io_stream.read(self.chunk_size) more_body = len(chunk) == self.chunk_size await send({ 'type': 'http.response.body', @@ -292,6 +292,69 @@ async def _send_body(self, send): 'more_body': more_body, }) + def rsgi(self, protocol: HTTPProtocol): + protocol.response_bytes( + self.status_code, + list(self.rsgi_headers), + self.io_stream.read() + ) + + +class HTTPIter(HTTPResponse): + def __init__( + self, + iter: Iterable[bytes], + headers: Dict[str, str] = {}, + cookies: Dict[str, Any] = {} + ): + super().__init__(200, headers=headers, cookies=cookies) + self.iter = iter + + async def _send_body(self, send): + for chunk in self.iter: + await send({ + 'type': 'http.response.body', + 'body': chunk, + 'more_body': True + }) + await send({'type': 'http.response.body', 'body': b'', 'more_body': False}) + + async def rsgi(self, protocol: HTTPProtocol): + trx = protocol.response_stream( + self.status_code, + list(self.rsgi_headers) + ) + for chunk in self.iter: + await trx.send_bytes(chunk) + + +class HTTPAiter(HTTPResponse): + def __init__( + self, + iter: AsyncIterable[bytes], + headers: Dict[str, str] = {}, + cookies: Dict[str, Any] = {} + ): + super().__init__(200, headers=headers, cookies=cookies) + self.iter = iter + + async def _send_body(self, send): + async for chunk in self.iter: + await send({ + 'type': 'http.response.body', + 'body': chunk, + 'more_body': True + }) + await send({'type': 'http.response.body', 'body': b'', 'more_body': False}) + + async def rsgi(self, protocol: HTTPProtocol): + trx = protocol.response_stream( + self.status_code, + list(self.rsgi_headers) + ) + async for chunk in self.iter: + await trx.send_bytes(chunk) + def redirect(location: str, status_code: int = 303): response = current.response diff --git a/emmett/orm/adapters.py b/emmett/orm/adapters.py index 7443dfc3..089a49a5 100644 --- a/emmett/orm/adapters.py +++ b/emmett/orm/adapters.py @@ -13,7 +13,6 @@ from functools import wraps -from pydal.adapters import adapters from pydal.adapters.base import SQLAdapter from pydal.adapters.mssql import ( MSSQL1, diff --git a/emmett/rsgi/handlers.py b/emmett/rsgi/handlers.py index 433b0d8c..abd9de12 100644 --- a/emmett/rsgi/handlers.py +++ b/emmett/rsgi/handlers.py @@ -95,7 +95,8 @@ async def __call__( self.app.log.warn( f"Timeout sending response: ({scope.path})" ) - http.rsgi(protocol) + if coro := http.rsgi(protocol): + await coro @cachedprop def error_handler(self) -> Callable[[], Awaitable[str]]: diff --git a/emmett/server.py b/emmett/server.py index 2122e2bb..468942fa 100644 --- a/emmett/server.py +++ b/emmett/server.py @@ -20,6 +20,7 @@ def run( host='127.0.0.1', port=8000, loop='auto', + loop_opt=False, log_level=None, workers=1, threads=1, @@ -40,6 +41,7 @@ def run( pthreads=threads, threading_mode=threading_mode, loop=loop, + loop_opt=loop_opt, websockets=enable_websockets, backlog=backlog, log_level=log_level, diff --git a/emmett/wrappers/request.py b/emmett/wrappers/request.py index 2aa2ccb7..578219f7 100644 --- a/emmett/wrappers/request.py +++ b/emmett/wrappers/request.py @@ -92,6 +92,16 @@ def _load_params_form_urlencoded(self, data): def _multipart_headers(self): return self.headers + @staticmethod + def _file_param_from_field(field): + return FileStorage( + BytesIO(field.file.read()), + field.filename, + field.name, + field.type, + field.headers + ) + def _load_params_form_multipart(self, data): params, files = sdict(), sdict() field_storage = FieldStorage( @@ -104,23 +114,21 @@ def _load_params_form_multipart(self, data): field = field_storage[key] if isinstance(field, list): if len(field) > 1: - params[key] = [] - for element in field: - params[key].append(element.value) + pvalues, fvalues = [], [] + for item in field: + if item.filename is not None: + fvalues.append(self._file_param_from_field(item)) + else: + pvalues.append(item.value) + if pvalues: + params[key] = pvalues + if fvalues: + files[key] = fvalues + continue else: - params[key] = field[0].value - elif ( - isinstance(field, FieldStorage) and - field.filename is not None - ): - files[key] = FileStorage( - BytesIO(field.file.read()), - field.filename, - field.name, - field.type, - field.headers - ) - continue + field = field[0] + if field.filename is not None: + files[key] = self._file_param_from_field(field) else: params[key] = field.value return params, files diff --git a/pyproject.toml b/pyproject.toml index 4ba696bf..d846c013 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "emmett" -version = "2.5.2" +version = "2.5.3" description = "The web framework for inventors" authors = ["Giovanni Barillari "] license = "BSD-3-Clause" @@ -43,7 +43,7 @@ emmett = "emmett.cli:main" [tool.poetry.dependencies] python = "^3.8" click = ">=6.0" -granian = "~0.4.2" +granian = "~0.5.0" emmett-crypto = "~0.3" pendulum = "~2.1.2" pyDAL = "17.3"