././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736456696.3452883 quart_trio-0.12.0/LICENSE0000644000000000000000000000203214740034770011721 0ustar00Copyright P G Jones 2018. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736456696.3452883 quart_trio-0.12.0/README.rst0000644000000000000000000000531214740034770012407 0ustar00Quart-Trio ========== |Build Status| |docs| |pypi| |python| |license| Quart-Trio is an extension for `Quart `__ to support the `Trio `_ event loop. This is an alternative to using the asyncio event loop present in the Python standard library and supported by default in Quart. Quickstart ---------- QuartTrio can be installed via `pip `_, .. code-block:: console $ pip install quart-trio and requires Python 3.8 or higher (see `python version support `_ for reasoning). A minimal Quart example is, .. code-block:: python from quart import websocket from quart_trio import QuartTrio app = QuartTrio(__name__) @app.route('/') async def hello(): return 'hello' @app.websocket('/ws') async def ws(): while True: await websocket.send('hello') app.run() if the above is in a file called ``app.py`` it can be run as, .. code-block:: console $ python app.py To deploy in a production setting see the `deployment `_ documentation. Contributing ------------ Quart-Trio is developed on `GitHub `_. You are very welcome to open `issues `_ or propose `merge requests `_. Testing ~~~~~~~ The best way to test Quart-Trio is with Tox, .. code-block:: console $ pip install tox $ tox this will check the code style and run the tests. Help ---- The `Quart-Trio `__ and `Quart `__ documentation are the best places to start, after that try searching `stack overflow `_, if you still can't find an answer please `open an issue `_. .. |Build Status| image:: https://github.com/pgjones/quart-trio/actions/workflows/ci.yml/badge.svg :target: https://github.com/pgjones/quart-trio/commits/main .. |docs| image:: https://img.shields.io/badge/docs-passing-brightgreen.svg :target: https://quart-trio.readthedocs.io .. |pypi| image:: https://img.shields.io/pypi/v/quart-trio.svg :target: https://pypi.python.org/pypi/Quart-Trio/ .. |python| image:: https://img.shields.io/pypi/pyversions/quart-trio.svg :target: https://pypi.python.org/pypi/Quart-Trio/ .. |license| image:: https://img.shields.io/badge/license-MIT-blue.svg :target: https://github.com/pgjones/quart-trio/blob/main/LICENSE ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736456714.1015356 quart_trio-0.12.0/pyproject.toml0000644000000000000000000000441214740035012013622 0ustar00[project] name = "quart-trio" version = "0.12.0" description = "A Quart extension to provide trio support" authors = [ { name = "pgjones", email = "philip.graham.jones@googlemail.com" }, ] classifiers = [ "Development Status :: 3 - Alpha", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Software Development :: Libraries :: Python Modules", ] include = [ "src/quart_trio/py.typed", ] readme = "README.rst" repository = "https://github.com/pgjones/quart-trio/" dependencies = [ "exceptiongroup >= 1.1.0; python_version < '3.11'", "hypercorn[trio] >= 0.12.0", "quart >= 0.19", "trio >= 0.19.0", ] requires-python = ">=3.9" [project.license] text = "MIT" [project.optional-dependencies] docs = [ "pydata_sphinx_theme", ] [tool.black] line-length = 100 target-version = [ "py39", ] [tool.isort] combine_as_imports = true force_grid_wrap = 0 include_trailing_comma = true known_first_party = "quart_trio, tests" line_length = 100 multi_line_output = 3 no_lines_before = "LOCALFOLDER" order_by_type = false reverse_relative = true [tool.mypy] allow_redefinition = true disallow_any_generics = false disallow_subclassing_any = true disallow_untyped_calls = false disallow_untyped_defs = true implicit_reexport = true no_implicit_optional = true show_error_codes = true strict = true strict_equality = true strict_optional = false warn_redundant_casts = true warn_return_any = false warn_unused_configs = true warn_unused_ignores = true [[tool.mypy.overrides]] module = [ "trio.*", ] ignore_missing_imports = true [tool.pytest.ini_options] addopts = "--no-cov-on-fail --showlocals --strict-markers" asyncio_default_fixture_loop_scope = "session" asyncio_mode = "auto" testpaths = [ "tests", ] [build-system] requires = [ "pdm-backend", ] build-backend = "pdm.backend" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736456696.3492882 quart_trio-0.12.0/src/quart_trio/__init__.py0000644000000000000000000000006514740034770016011 0ustar00from .app import QuartTrio __all__ = ("QuartTrio",) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736456696.3492882 quart_trio-0.12.0/src/quart_trio/app.py0000644000000000000000000002743514740034770015044 0ustar00import sys import warnings from typing import Any, Awaitable, Callable, Coroutine, Optional, TypeVar, Union import trio from hypercorn.config import Config as HyperConfig from hypercorn.trio import serve from quart import Quart, request_started, websocket_started from quart.ctx import RequestContext, WebsocketContext from quart.signals import got_serving_exception from quart.typing import FilePath, ResponseReturnValue from quart.utils import file_path_to_path from quart.wrappers import Request, Response, Websocket from werkzeug.exceptions import HTTPException from werkzeug.wrappers import Response as WerkzeugResponse from .asgi import TrioASGIHTTPConnection, TrioASGILifespan, TrioASGIWebsocketConnection from .testing import TrioClient, TrioTestApp from .utils import run_sync from .wrappers import TrioRequest, TrioResponse, TrioWebsocket try: from typing import ParamSpec except ImportError: from typing_extensions import ParamSpec if sys.version_info < (3, 11): from exceptiongroup import BaseExceptionGroup T = TypeVar("T") P = ParamSpec("P") class QuartTrio(Quart): nursery: trio.Nursery asgi_http_class = TrioASGIHTTPConnection asgi_lifespan_class = TrioASGILifespan asgi_websocket_class = TrioASGIWebsocketConnection event_class = trio.Event # type: ignore lock_class = trio.Lock # type: ignore request_class = TrioRequest response_class = TrioResponse test_app_class = TrioTestApp test_client_class = TrioClient # type: ignore websocket_class = TrioWebsocket def run( # type: ignore self, host: str = "127.0.0.1", port: int = 5000, debug: Optional[bool] = None, use_reloader: bool = True, ca_certs: Optional[str] = None, certfile: Optional[str] = None, keyfile: Optional[str] = None, **kwargs: Any, ) -> None: """Run this application. This is best used for development only, see using Gunicorn for production servers. Arguments: host: Hostname to listen on. By default this is loopback only, use 0.0.0.0 to have the server listen externally. port: Port number to listen on. debug: If set enable (or disable) debug mode and debug output. use_reloader: Automatically reload on code changes. ca_certs: Path to the SSL CA certificate file. certfile: Path to the SSL certificate file. ciphers: Ciphers to use for the SSL setup. keyfile: Path to the SSL key file. """ if kwargs: warnings.warn( f"Additional arguments, {','.join(kwargs.keys())}, are not supported.\n" "They may be supported by Hypercorn, which is the ASGI server Quart " "uses by default. This method is meant for development and debugging." ) scheme = "https" if certfile is not None and keyfile is not None else "http" print(f"Running on {scheme}://{host}:{port} (CTRL + C to quit)") # noqa: T201 trio.run(self.run_task, host, port, debug, ca_certs, certfile, keyfile) def run_task( self, host: str = "127.0.0.1", port: int = 5000, debug: Optional[bool] = None, ca_certs: Optional[str] = None, certfile: Optional[str] = None, keyfile: Optional[str] = None, shutdown_trigger: Optional[Callable[..., Awaitable[None]]] = None, task_status: trio.TaskStatus = trio.TASK_STATUS_IGNORED, ) -> Coroutine[None, None, None]: """Return a task that when awaited runs this application. This is best used for development only, see Hypercorn for production servers. Arguments: host: Hostname to listen on. By default this is loopback only, use 0.0.0.0 to have the server listen externally. port: Port number to listen on. debug: If set enable (or disable) debug mode and debug output. use_reloader: Automatically reload on code changes. loop: Asyncio loop to create the server in, if None, take default one. If specified it is the caller's responsibility to close and cleanup the loop. ca_certs: Path to the SSL CA certificate file. certfile: Path to the SSL certificate file. keyfile: Path to the SSL key file. """ config = HyperConfig() config.access_log_format = "%(h)s %(r)s %(s)s %(b)s %(D)s" config.accesslog = "-" config.bind = [f"{host}:{port}"] config.ca_certs = ca_certs config.certfile = certfile if debug is not None: config.debug = debug config.errorlog = config.accesslog config.keyfile = keyfile return serve( self, config, shutdown_trigger=shutdown_trigger, task_status=task_status # type: ignore ) def sync_to_async(self, func: Callable[P, T]) -> Callable[P, Awaitable[T]]: """Return a async function that will run the synchronous function *func*. This can be used as so,:: result = await app.sync_to_async(func)(*args, **kwargs) Override this method to change how the app converts sync code to be asynchronously callable. """ return run_sync(func) async def handle_request(self, request: Request) -> Union[Response, WerkzeugResponse]: async with self.request_context(request) as request_context: try: return await self.full_dispatch_request(request_context) except trio.Cancelled: raise # Cancelled should be handled by serving code. except BaseExceptionGroup as error: filtered_error, _ = error.split(trio.Cancelled) if filtered_error is not None: raise filtered_error return await self.handle_exception(error) # type: ignore except Exception as error: return await self.handle_exception(error) finally: if request.scope.get("_quart._preserve_context", False): self._preserved_context = request_context.copy() async def full_dispatch_request( self, request_context: Optional[RequestContext] = None ) -> Union[Response, WerkzeugResponse]: """Adds pre and post processing to the request dispatching. Arguments: request_context: The request context, optional as Flask omits this argument. """ try: await request_started.send_async(self, _sync_wrapper=self.ensure_async) # type: ignore result: ResponseReturnValue | HTTPException | None result = await self.preprocess_request(request_context) if result is None: result = await self.dispatch_request(request_context) except (Exception, BaseExceptionGroup) as error: result = await self.handle_user_exception(error) return await self.finalize_request(result, request_context) async def handle_user_exception( self, error: Union[Exception, BaseExceptionGroup] ) -> Union[HTTPException, ResponseReturnValue]: if isinstance(error, BaseExceptionGroup): for exception in error.exceptions: try: return await self.handle_user_exception(exception) # type: ignore except Exception: pass # No handler for this error # Not found a single handler, re-raise the error raise error else: return await super().handle_user_exception(error) async def handle_websocket( self, websocket: Websocket ) -> Optional[Union[Response, WerkzeugResponse]]: async with self.websocket_context(websocket) as websocket_context: try: return await self.full_dispatch_websocket(websocket_context) except trio.Cancelled: raise # Cancelled should be handled by serving code. except BaseExceptionGroup as error: filtered_error, _ = error.split(trio.Cancelled) if filtered_error is not None: raise filtered_error return await self.handle_websocket_exception(error) # type: ignore except Exception as error: return await self.handle_websocket_exception(error) finally: if websocket.scope.get("_quart._preserve_context", False): self._preserved_context = websocket_context.copy() async def full_dispatch_websocket( self, websocket_context: Optional[WebsocketContext] = None ) -> Optional[Union[Response, WerkzeugResponse]]: """Adds pre and post processing to the websocket dispatching. Arguments: websocket_context: The websocket context, optional to match the Flask convention. """ try: await websocket_started.send_async( self, _sync_wrapper=self.ensure_async # type: ignore ) result: ResponseReturnValue | HTTPException | None result = await self.preprocess_websocket(websocket_context) if result is None: result = await self.dispatch_websocket(websocket_context) except (Exception, BaseExceptionGroup) as error: result = await self.handle_user_exception(error) return await self.finalize_websocket(result, websocket_context) async def open_instance_resource( self, path: FilePath, mode: str = "rb" ) -> trio._file_io.AsyncIOWrapper: """Open a file for reading. Use as .. code-block:: python async with await app.open_instance_resource(path) as file_: await file_.read() """ return await trio.open_file(self.instance_path / file_path_to_path(path), mode) async def open_resource(self, path: FilePath, mode: str = "rb") -> trio._file_io.AsyncIOWrapper: """Open a file for reading. Use as .. code-block:: python async with await app.open_resource(path) as file_: await file_.read() """ if mode not in {"r", "rb"}: raise ValueError("Files can only be opened for reading") return await trio.open_file(self.root_path / file_path_to_path(path), mode) def add_background_task(self, func: Callable, *args: Any, **kwargs: Any) -> None: async def _wrapper() -> None: try: async with self.app_context(): await self.ensure_async(func)(*args, **kwargs) except (BaseExceptionGroup, Exception) as error: await self.handle_background_exception(error) # type: ignore self.nursery.start_soon(_wrapper) async def shutdown(self) -> None: if self.config["BACKGROUND_TASK_SHUTDOWN_TIMEOUT"] is not None: self.nursery.cancel_scope.deadline = ( trio.current_time() + self.config["BACKGROUND_TASK_SHUTDOWN_TIMEOUT"] ) else: self.nursery.cancel_scope.cancel() try: async with self.app_context(): for func in self.after_serving_funcs: await self.ensure_async(func)() for gen in self.while_serving_gens: try: await gen.__anext__() except StopAsyncIteration: pass else: raise RuntimeError("While serving generator didn't terminate") except Exception as error: await got_serving_exception.send_async( self, _sync_wrapper=self.ensure_async, exception=error # type: ignore ) self.log_exception(sys.exc_info()) raise ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736456696.3492882 quart_trio-0.12.0/src/quart_trio/asgi.py0000644000000000000000000001517514740034770015205 0ustar00import sys from functools import partial from typing import cast, Optional, TYPE_CHECKING, Union from urllib.parse import urlparse import trio from hypercorn.typing import ( ASGIReceiveCallable, ASGISendCallable, LifespanScope, LifespanShutdownCompleteEvent, LifespanShutdownFailedEvent, LifespanStartupCompleteEvent, LifespanStartupFailedEvent, WebsocketScope, ) from quart.asgi import ASGIHTTPConnection, ASGIWebsocketConnection from quart.signals import websocket_received from quart.wrappers import Request, Response, Websocket # noqa: F401 from werkzeug.datastructures import Headers if TYPE_CHECKING: from quart_trio import QuartTrio # noqa: F401 if sys.version_info < (3, 11): from exceptiongroup import BaseExceptionGroup class TrioASGIHTTPConnection(ASGIHTTPConnection): async def __call__(self, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: request = self._create_request_from_scope(send) async with trio.open_nursery() as nursery: nursery.start_soon(self.handle_messages, nursery, request, receive) nursery.start_soon(self.handle_request, nursery, request, send) async def handle_messages( # type: ignore self, nursery: trio.Nursery, request: Request, receive: ASGIReceiveCallable ) -> None: await super().handle_messages(request, receive) nursery.cancel_scope.cancel() async def handle_request( # type: ignore self, nursery: trio.Nursery, request: Request, send: ASGISendCallable ) -> None: response = await self.app.handle_request(request) if isinstance(response, Response) and response.timeout != Ellipsis: timeout = cast(Optional[float], response.timeout) else: timeout = self.app.config["RESPONSE_TIMEOUT"] if timeout is not None: with trio.move_on_after(timeout): await self._send_response(send, response) else: await self._send_response(send, response) nursery.cancel_scope.cancel() class TrioASGIWebsocketConnection(ASGIWebsocketConnection): def __init__(self, app: "QuartTrio", scope: WebsocketScope) -> None: self.app = app self.scope = scope self._accepted = False self._closed = False self.send_channel, self.receive_channel = trio.open_memory_channel[Union[bytes, str]](10) async def __call__(self, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: websocket = self._create_websocket_from_scope(send) async with trio.open_nursery() as nursery: nursery.start_soon(self.handle_messages, nursery, receive) nursery.start_soon(self.handle_websocket, nursery, websocket, send) async def handle_messages( # type: ignore self, nursery: trio.Nursery, receive: ASGIReceiveCallable ) -> None: while True: event = await receive() if event["type"] == "websocket.receive": message = event.get("bytes") or event["text"] await websocket_received.send_async( message, _sync_wrapper=self.app.ensure_async # type: ignore ) await self.send_channel.send(message) elif event["type"] == "websocket.disconnect": break nursery.cancel_scope.cancel() def _create_websocket_from_scope(self, send: ASGISendCallable) -> Websocket: headers = Headers() headers["Remote-Addr"] = (self.scope.get("client") or [""])[0] for name, value in self.scope["headers"]: headers.add(name.decode("latin1").title(), value.decode("latin1")) path = self.scope["path"] path = path if path[0] == "/" else urlparse(path).path root_path = self.scope.get("root_path", "") if root_path != "": try: path = path.split(root_path, 1)[1] path = " " if path == "" else path except IndexError: path = " " # Invalid in paths, hence will result in 404 return self.app.websocket_class( path, self.scope["query_string"], self.scope["scheme"], headers, self.scope.get("root_path", ""), self.scope.get("http_version", "1.1"), list(self.scope.get("subprotocols", [])), self.receive_channel.receive, partial(self.send_data, send), partial(self.accept_connection, send), partial(self.close_connection, send), scope=self.scope, ) async def handle_websocket( # type: ignore self, nursery: trio.Nursery, websocket: Websocket, send: ASGISendCallable ) -> None: await super().handle_websocket(websocket, send) nursery.cancel_scope.cancel() class TrioASGILifespan: def __init__(self, app: "QuartTrio", scope: LifespanScope) -> None: self.app = app async def __call__(self, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: async with trio.open_nursery() as nursery: self.app.nursery = nursery while True: event = await receive() if event["type"] == "lifespan.startup": try: await self.app.startup() except (Exception, BaseExceptionGroup) as error: await send( cast( LifespanStartupFailedEvent, {"type": "lifespan.startup.failed", "message": str(error)}, ), ) else: await send( cast( LifespanStartupCompleteEvent, {"type": "lifespan.startup.complete"} ) ) elif event["type"] == "lifespan.shutdown": try: await self.app.shutdown() except (Exception, BaseExceptionGroup) as error: await send( cast( LifespanShutdownFailedEvent, {"type": "lifespan.shutdown.failed", "message": str(error)}, ), ) else: await send( cast( LifespanShutdownCompleteEvent, {"type": "lifespan.shutdown.complete"}, ), ) break ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736456696.3492882 quart_trio-0.12.0/src/quart_trio/datastructures.py0000644000000000000000000000177214740034770017335 0ustar00from __future__ import annotations from os import PathLike from quart.datastructures import FileStorage from trio import open_file, Path, wrap_file class TrioFileStorage(FileStorage): async def save(self, destination: PathLike, buffer_size: int = 16384) -> None: # type: ignore wrapped_stream = wrap_file(self.stream) async with await open_file(destination, "wb") as file_: data = await wrapped_stream.read(buffer_size) while data != b"": await file_.write(data) data = await wrapped_stream.read(buffer_size) async def load(self, source: PathLike, buffer_size: int = 16384) -> None: path = Path(source) self.filename = path.name wrapped_stream = wrap_file(self.stream) async with await open_file(path, "rb") as file_: data = await file_.read(buffer_size) while data != b"": await wrapped_stream.write(data) data = await file_.read(buffer_size) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736456696.3492882 quart_trio-0.12.0/src/quart_trio/formparser.py0000644000000000000000000000027014740034770016430 0ustar00from quart.formparser import FormDataParser from quart_trio.datastructures import TrioFileStorage class TrioFormDataParser(FormDataParser): file_storage_class = TrioFileStorage ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736456696.3492882 quart_trio-0.12.0/src/quart_trio/py.typed0000644000000000000000000000000714740034770015373 0ustar00Marker ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736456696.3492882 quart_trio-0.12.0/src/quart_trio/testing/__init__.py0000644000000000000000000000021114740034770017457 0ustar00from __future__ import annotations from .app import TrioTestApp from .client import TrioClient __all__ = ("TrioClient", "TrioTestApp") ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736456696.3492882 quart_trio-0.12.0/src/quart_trio/testing/app.py0000644000000000000000000000511014740034770016503 0ustar00from __future__ import annotations from contextlib import AbstractAsyncContextManager from types import TracebackType import trio from quart.app import Quart from quart.testing.app import DEFAULT_TIMEOUT, LifespanError from quart.typing import TestClientProtocol class TrioTestApp: def __init__( self, app: "Quart", startup_timeout: int = DEFAULT_TIMEOUT, shutdown_timeout: int = DEFAULT_TIMEOUT, ) -> None: self.app = app self.startup_timeout = startup_timeout self.shutdown_timeout = shutdown_timeout self._startup = trio.Event() self._shutdown = trio.Event() self._app_send_channel, self._app_receive_channel = trio.open_memory_channel[dict](10) self._nursery_manager: AbstractAsyncContextManager[trio.Nursery] self._nursery: trio.Nursery def test_client(self) -> TestClientProtocol: return self.app.test_client() async def startup(self) -> None: scope = {"type": "lifespan", "asgi": {"spec_version": "2.0"}} self._nursery.start_soon(self.app, scope, self._asgi_receive, self._asgi_send) # type: ignore # noqa: E501 await self._app_send_channel.send({"type": "lifespan.startup"}) with trio.fail_after(self.startup_timeout): await self._startup.wait() async def shutdown(self) -> None: await self._app_send_channel.send({"type": "lifespan.shutdown"}) with trio.fail_after(self.shutdown_timeout): await self._shutdown.wait() async def __aenter__(self) -> "TrioTestApp": self._nursery_manager = trio.open_nursery() self._nursery = await self._nursery_manager.__aenter__() await self.startup() return self async def __aexit__(self, exc_type: type, exc_value: BaseException, tb: TracebackType) -> None: await self.shutdown() await self._nursery_manager.__aexit__(exc_type, exc_value, tb) async def _asgi_receive(self) -> dict: return await self._app_receive_channel.receive() async def _asgi_send(self, message: dict) -> None: if message["type"] == "lifespan.startup.complete": self._startup.set() elif message["type"] == "lifespan.shutdown.complete": self._shutdown.set() elif message["type"] == "lifespan.startup.failed": self._startup.set() raise LifespanError(f"Error during startup {message['message']}") elif message["type"] == "lifespan.shutdown.failed": self._shutdown.set() raise LifespanError(f"Error during shutdown {message['message']}") ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736456696.3492882 quart_trio-0.12.0/src/quart_trio/testing/client.py0000644000000000000000000000072714740034770017212 0ustar00from __future__ import annotations from typing import Type from quart.testing.client import QuartClient from quart.typing import TestHTTPConnectionProtocol, TestWebsocketConnectionProtocol from .connections import TestHTTPConnection, TestWebsocketConnection class TrioClient(QuartClient): http_connection_class: Type[TestHTTPConnectionProtocol] = TestHTTPConnection websocket_connection_class: Type[TestWebsocketConnectionProtocol] = TestWebsocketConnection ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736456696.3492882 quart_trio-0.12.0/src/quart_trio/testing/connections.py0000644000000000000000000001625514740034770020261 0ustar00from __future__ import annotations from types import TracebackType from typing import Any, AnyStr, Awaitable, List, Optional, Tuple, Union import trio from hypercorn.typing import HTTPScope, WebsocketScope from quart.app import Quart from quart.json import dumps, loads from quart.testing.connections import ( HTTPDisconnectError, WebsocketDisconnectError, WebsocketResponseError, ) from quart.typing import TestHTTPConnectionProtocol, TestWebsocketConnectionProtocol from quart.utils import decode_headers from quart.wrappers import Response from werkzeug.datastructures import Headers class TestHTTPConnection: def __init__(self, app: Quart, scope: HTTPScope, _preserve_context: bool = False) -> None: self.app = app self.headers: Optional[Headers] = None self.push_promises: List[Tuple[str, Headers]] = [] self.response_data = bytearray() self.scope = scope self.status_code: Optional[int] = None self._preserve_context = _preserve_context self._server_send, self._server_receive = trio.open_memory_channel[dict](10) self._client_send, self._client_receive = trio.open_memory_channel[Union[bytes, Exception]]( 10 ) async def send(self, data: bytes) -> None: await self._server_send.send({"type": "http.request", "body": data, "more_body": True}) async def send_complete(self) -> None: await self._server_send.send({"type": "http.request", "body": b"", "more_body": False}) async def receive(self) -> bytes: data = await self._client_receive.receive() if isinstance(data, Exception): raise data else: return data async def disconnect(self) -> None: await self._server_send.send({"type": "http.disconnect"}) await self._server_send.aclose() async def __aenter__(self) -> TestHTTPConnectionProtocol: self._nursery_manager = trio.open_nursery() nursery = await self._nursery_manager.__aenter__() nursery.start_soon(self.app, self.scope, self._asgi_receive, self._asgi_send) # type: ignore # noqa: E501 return self async def __aexit__(self, exc_type: type, exc_value: BaseException, tb: TracebackType) -> None: if exc_type is not None: await self.disconnect() await self._nursery_manager.__aexit__(exc_type, exc_value, tb) try: async with self._client_receive: async for data in self._client_receive: if isinstance(data, bytes): self.response_data.extend(data) elif not isinstance(data, HTTPDisconnectError): raise data except trio.ClosedResourceError: pass async def as_response(self) -> Response: try: async with self._client_receive: async for data in self._client_receive: if isinstance(data, bytes): self.response_data.extend(data) except trio.ClosedResourceError: pass return self.app.response_class(bytes(self.response_data), self.status_code, self.headers) async def _asgi_receive(self) -> dict: return await self._server_receive.receive() async def _asgi_send(self, message: dict) -> None: if message["type"] == "http.response.start": self.headers = decode_headers(message["headers"]) self.status_code = message["status"] elif message["type"] == "http.response.body": await self._client_send.send(message["body"]) if not message.get("more_body", False): await self._client_send.aclose() elif message["type"] == "http.response.push": self.push_promises.append((message["path"], decode_headers(message["headers"]))) elif message["type"] == "http.disconnect": await self._client_send.send(HTTPDisconnectError()) await self._client_send.aclose() class TestWebsocketConnection: def __init__(self, app: Quart, scope: WebsocketScope) -> None: self.accepted = False self.app = app self.headers: Optional[Headers] = None self.response_data = bytearray() self.scope = scope self.status_code: Optional[int] = None self._server_send, self._server_receive = trio.open_memory_channel[dict](10) self._client_send, self._client_receive = trio.open_memory_channel[ Union[bytes, str, Exception] ](10) self._task: Awaitable[None] = None async def __aenter__(self) -> TestWebsocketConnectionProtocol: self._nursery_manager = trio.open_nursery() nursery = await self._nursery_manager.__aenter__() nursery.start_soon(self.app, self.scope, self._asgi_receive, self._asgi_send) # type: ignore # noqa: E501 return self async def __aexit__(self, exc_type: type, exc_value: BaseException, tb: TracebackType) -> None: await self.disconnect() await self._nursery_manager.__aexit__(exc_type, exc_value, tb) async def receive(self) -> AnyStr: data = await self._client_receive.receive() if isinstance(data, Exception): raise data else: return data # type: ignore async def send(self, data: AnyStr) -> None: if isinstance(data, str): await self._server_send.send({"type": "websocket.receive", "text": data}) else: await self._server_send.send({"type": "websocket.receive", "bytes": data}) async def receive_json(self) -> Any: data = await self.receive() return loads(data) async def send_json(self, data: Any) -> None: raw = dumps(data) await self.send(raw) async def close(self, code: int) -> None: await self._server_send.send({"type": "websocket.close", "code": int}) await self._server_send.aclose() async def disconnect(self) -> None: await self._server_send.send({"type": "websocket.disconnect"}) await self._server_send.aclose() async def _asgi_receive(self) -> dict: return await self._server_receive.receive() async def _asgi_send(self, message: dict) -> None: if message["type"] == "websocket.accept": self.accepted = True elif message["type"] == "websocket.send": await self._client_send.send(message.get("bytes") or message.get("text")) elif message["type"] == "websocket.http.response.start": self.headers = decode_headers(message["headers"]) self.status_code = message["status"] elif message["type"] == "websocket.http.response.body": self.response_data.extend(message["body"]) if not message.get("more_body", False): await self._client_send.send( WebsocketResponseError( self.app.response_class( bytes(self.response_data), self.status_code, self.headers ) ) ) elif message["type"] == "websocket.close": await self._client_send.send(WebsocketDisconnectError(message.get("code", 1000))) await self._client_send.aclose() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736456696.3492882 quart_trio-0.12.0/src/quart_trio/utils.py0000644000000000000000000000324314740034770015413 0ustar00from contextvars import copy_context from functools import partial, wraps from inspect import isgenerator from typing import Any, AsyncGenerator, Callable, Coroutine, Generator import trio def run_sync(func: Callable[..., Any]) -> Callable[..., Coroutine[Any, None, None]]: """Ensure that the sync function is run without blocking. If the *func* is not a coroutine it will be wrapped such that it runs on a thread. This ensures that synchronous functions do not block the event loop. """ @wraps(func) async def _wrapper(*args: Any, **kwargs: Any) -> Any: result = await trio.to_thread.run_sync(copy_context().run, partial(func, *args, **kwargs)) if isgenerator(result): return run_sync_iterable(result) else: return result _wrapper._quart_async_wrapper = True # type: ignore return _wrapper def run_sync_iterable(iterable: Generator[Any, None, None]) -> AsyncGenerator[Any, None]: async def _gen_wrapper() -> AsyncGenerator[Any, None]: # Wrap the generator such that each iteration runs # in the executor. Then rationalise the raised # errors so that it ends. def _inner() -> Any: # https://bugs.python.org/issue26221 # StopIteration errors are swallowed by the # run_in_exector method try: return next(iterable) except StopIteration: raise StopAsyncIteration() while True: try: yield await trio.to_thread.run_sync(partial(copy_context().run, _inner)) except StopAsyncIteration: return return _gen_wrapper() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736456696.3492882 quart_trio-0.12.0/src/quart_trio/wrappers/__init__.py0000644000000000000000000000026414740034770017655 0ustar00from .request import TrioRequest from .response import TrioResponse from .websocket import TrioWebsocket __all__ = ( "TrioRequest", "TrioResponse", "TrioWebsocket", ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736456696.3492882 quart_trio-0.12.0/src/quart_trio/wrappers/request.py0000644000000000000000000000623614740034770017613 0ustar00from typing import Literal, Optional, overload, Union import trio from quart.wrappers.request import Body, Request from werkzeug.exceptions import RequestEntityTooLarge, RequestTimeout from ..formparser import TrioFormDataParser class EventWrapper: def __init__(self) -> None: self._event = trio.Event() def clear(self) -> None: self._event = trio.Event() async def wait(self) -> None: await self._event.wait() def is_set(self) -> bool: return self._event.is_set() def set(self) -> None: self._event.set() class TrioBody(Body): def __init__( self, expected_content_length: Optional[int], max_content_length: Optional[int] ) -> None: self._data = bytearray() self._complete = EventWrapper() # type: ignore self._has_data = EventWrapper() # type: ignore self._max_content_length = max_content_length # Exceptions must be raised within application (not ASGI) # calls, this is achieved by having the ASGI methods set this # to an exception on error. self._must_raise: Optional[Exception] = None if ( expected_content_length is not None and max_content_length is not None and expected_content_length > max_content_length ): self._must_raise = RequestEntityTooLarge() class TrioRequest(Request): body_class = TrioBody form_data_parser_class = TrioFormDataParser lock_class = trio.Lock # type: ignore @overload async def get_data( self, cache: bool, as_text: Literal[False], parse_form_data: bool ) -> bytes: ... @overload async def get_data(self, cache: bool, as_text: Literal[True], parse_form_data: bool) -> str: ... @overload async def get_data( self, cache: bool = True, as_text: bool = False, parse_form_data: bool = False ) -> Union[str, bytes]: ... async def get_data( self, cache: bool = True, as_text: bool = False, parse_form_data: bool = False ) -> Union[str, bytes]: if parse_form_data: await self._load_form_data() timeout = float("inf") if self.body_timeout is None else self.body_timeout with trio.move_on_after(timeout) as cancel_scope: raw_data = await self.body if cancel_scope.cancelled_caught: raise RequestTimeout() if not cache: self.body.clear() if as_text: return raw_data.decode() else: return raw_data async def _load_form_data(self) -> None: async with self._parsing_lock: if self._form is None: parser = self.make_form_data_parser() timeout = float("inf") if self.body_timeout is None else self.body_timeout with trio.move_on_after(timeout) as cancel_scope: self._form, self._files = await parser.parse( self.body, self.mimetype, self.content_length, self.mimetype_params, ) if cancel_scope.cancelled_caught: raise RequestTimeout() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736456696.3492882 quart_trio-0.12.0/src/quart_trio/wrappers/response.py0000644000000000000000000000611414740034770017754 0ustar00import os from inspect import isasyncgen, isgenerator from types import TracebackType from typing import AsyncGenerator, Iterable, Optional, Union import trio from quart.wrappers.response import _raise_if_invalid_range, IterableBody, Response, ResponseBody from ..utils import run_sync_iterable class TrioFileBody(ResponseBody): """Provides an async file accessor with range setting. The :attr:`Response.response` attribute must be async-iterable and yield bytes, which this wrapper does for a file. In addition it allows a range to be set on the file, thereby supporting conditional requests. """ buffer_size = 8192 def __init__( self, file_path: Union[str, bytes, os.PathLike], *, buffer_size: Optional[int] = None ) -> None: self.file_path = file_path self.size = os.path.getsize(self.file_path) self.begin = 0 self.end = self.size if buffer_size is not None: self.buffer_size = buffer_size self.file: Optional[trio._file_io.AsyncIOWrapper] = None async def __aenter__(self) -> "TrioFileBody": self.file = await trio.open_file(self.file_path, mode="rb") await self.file.__aenter__() await self.file.seek(self.begin) return self async def __aexit__(self, exc_type: type, exc_value: BaseException, tb: TracebackType) -> None: await self.file.__aexit__(exc_type, exc_value, tb) def __aiter__(self) -> "TrioFileBody": return self async def __anext__(self) -> bytes: current = await self.file.tell() if current >= self.end: raise StopAsyncIteration() read_size = min(self.buffer_size, self.end - current) chunk = await self.file.read(read_size) if chunk: return chunk else: raise StopAsyncIteration() async def convert_to_sequence(self) -> bytes: result = bytearray() async with self as response: async for data in response: result.extend(data) return bytes(result) async def make_conditional( self, begin: int, end: Optional[int], max_partial_size: Optional[int] = None ) -> int: self.begin = begin self.end = self.size if end is None else end if max_partial_size is not None: self.end = min(self.begin + max_partial_size, self.end) _raise_if_invalid_range(self.begin, self.end, self.size) return self.size class TrioIterableBody(IterableBody): def __init__(self, iterable: Union[AsyncGenerator[bytes, None], Iterable]) -> None: self.iter: AsyncGenerator[bytes, None] if isasyncgen(iterable): self.iter = iterable elif isgenerator(iterable): self.iter = run_sync_iterable(iterable) else: async def _aiter() -> AsyncGenerator[bytes, None]: for data in iterable: # type: ignore yield data self.iter = _aiter() class TrioResponse(Response): file_body_class = TrioFileBody # type: ignore iterable_body_class = TrioIterableBody ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736456696.3492882 quart_trio-0.12.0/src/quart_trio/wrappers/websocket.py0000644000000000000000000000032714740034770020104 0ustar00from typing import AnyStr from quart.wrappers.websocket import Websocket class TrioWebsocket(Websocket): async def send(self, data: AnyStr) -> None: await self.accept() await self._send(data) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736456696.3492882 quart_trio-0.12.0/tests/test_app.py0000644000000000000000000000572614740034770014264 0ustar00import sys from typing import NoReturn import pytest from quart import ResponseReturnValue from quart.testing import WebsocketResponseError from quart_trio import QuartTrio if sys.version_info < (3, 11): from exceptiongroup import BaseExceptionGroup @pytest.fixture(name="error_app", scope="function") def _error_app() -> QuartTrio: app = QuartTrio(__name__) @app.route("/") async def index() -> NoReturn: raise BaseExceptionGroup( "msg1", [ValueError(), BaseExceptionGroup("msg2", [TypeError(), ValueError()])] ) @app.websocket("/ws/") async def ws() -> NoReturn: raise BaseExceptionGroup( "msg3", [ValueError(), BaseExceptionGroup("msg4", [TypeError(), ValueError()])] ) return app @pytest.mark.trio async def test_exception_group_handling(error_app: QuartTrio) -> None: @error_app.errorhandler(TypeError) async def handler(_: Exception) -> ResponseReturnValue: return "", 201 test_client = error_app.test_client() response = await test_client.get("/") assert response.status_code == 201 @pytest.mark.trio async def test_websocket_exception_group_handling(error_app: QuartTrio) -> None: @error_app.errorhandler(TypeError) async def handler(_: Exception) -> ResponseReturnValue: return "", 201 test_client = error_app.test_client() try: async with test_client.websocket("/ws/") as test_websocket: await test_websocket.receive() except BaseExceptionGroup as error: for exception in error.exceptions: if isinstance(exception, WebsocketResponseError): assert exception.response.status_code == 201 @pytest.mark.trio async def test_exception_group_unhandled(error_app: QuartTrio) -> None: test_client = error_app.test_client() response = await test_client.get("/") assert response.status_code == 500 @pytest.mark.trio async def test_websocket_exception_group_unhandled(error_app: QuartTrio) -> None: test_client = error_app.test_client() try: async with test_client.websocket("/ws/") as test_websocket: await test_websocket.receive() except BaseExceptionGroup as error: for exception in error.exceptions: if isinstance(exception, WebsocketResponseError): assert exception.response.status_code == 500 @pytest.mark.trio async def test_test_app() -> None: startup = False shutdown = False app = QuartTrio(__name__) @app.before_serving async def before() -> None: nonlocal startup startup = True @app.after_serving async def after() -> None: nonlocal shutdown shutdown = True @app.route("/") async def index() -> str: return "" async with app.test_app() as test_app: assert startup assert app.nursery is not None test_client = test_app.test_client() await test_client.get("/") assert not shutdown assert shutdown ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736456696.3492882 quart_trio-0.12.0/tests/test_asgi.py0000644000000000000000000000231714740034770014420 0ustar00import pytest import trio from hypercorn.typing import WebsocketScope from quart_trio.app import QuartTrio from quart_trio.asgi import TrioASGIWebsocketConnection @pytest.mark.trio async def test_websocket_complete_on_disconnect() -> None: scope: WebsocketScope = { "type": "websocket", "asgi": {}, "http_version": "1.1", "scheme": "wss", "path": "ws://quart/path", "raw_path": b"/", "query_string": b"", "root_path": "", "headers": [(b"host", b"quart")], "client": ("127.0.0.1", 80), "server": None, "subprotocols": [], "extensions": {"websocket.http.response": {}}, "state": {}, # type: ignore[typeddict-item] } connection = TrioASGIWebsocketConnection(QuartTrio(__name__), scope) send_channel, receive_channel = trio.open_memory_channel[dict](0) async with trio.open_nursery() as nursery: nursery.start_soon( connection.handle_messages, nursery, receive_channel.receive # type: ignore ) await send_channel.send({"type": "websocket.disconnect"}) await trio.sleep(1) # Simulate doing something else assert nursery.cancel_scope.cancelled_caught ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736456696.3492882 quart_trio-0.12.0/tests/test_basic.py0000644000000000000000000000402214740034770014551 0ustar00import sys from pathlib import Path import pytest from quart import abort, Quart, ResponseReturnValue, send_file, websocket from quart.testing import WebsocketResponseError from quart_trio import QuartTrio if sys.version_info < (3, 11): from exceptiongroup import BaseExceptionGroup @pytest.fixture def app() -> Quart: app = QuartTrio(__name__) @app.route("/") async def index() -> ResponseReturnValue: return "index" @app.websocket("/ws/") async def ws() -> None: # async for message in websocket: while True: message = await websocket.receive() await websocket.send(message) @app.websocket("/ws/abort/") async def ws_abort() -> None: abort(401) return app @pytest.mark.trio async def test_index(app: Quart) -> None: test_client = app.test_client() response = await test_client.get("/") assert response.status_code == 200 assert b"index" in (await response.get_data()) @pytest.mark.trio async def test_websocket(app: Quart) -> None: test_client = app.test_client() data = b"bob" async with test_client.websocket("/ws/") as test_websocket: await test_websocket.send(data) result = await test_websocket.receive() assert result == data # type: ignore @pytest.mark.trio async def test_websocket_abort(app: Quart) -> None: test_client = app.test_client() try: async with test_client.websocket("/ws/abort/") as test_websocket: await test_websocket.receive() except BaseExceptionGroup as error: for exception in error.exceptions: if isinstance(exception, WebsocketResponseError): assert exception.response.status_code == 401 @pytest.mark.trio async def test_send_file_path(tmp_path: Path) -> None: app = QuartTrio(__name__) file_ = tmp_path / "send.img" file_.write_text("something") async with app.app_context(): response = await send_file(file_) assert (await response.get_data(as_text=False)) == file_.read_bytes() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736456696.3492882 quart_trio-0.12.0/tests/test_request.py0000644000000000000000000000060014740034770015156 0ustar00import pytest from werkzeug.exceptions import RequestEntityTooLarge from quart_trio.wrappers.request import TrioBody @pytest.mark.trio async def test_body_exceeds_max_content_length() -> None: max_content_length = 5 body = TrioBody(None, max_content_length) body.append(b" " * (max_content_length + 1)) with pytest.raises(RequestEntityTooLarge): await body ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736456696.3492882 quart_trio-0.12.0/tests/test_sync.py0000644000000000000000000000227214740034770014451 0ustar00import threading from typing import Generator import pytest from quart import request, ResponseReturnValue from quart_trio import QuartTrio @pytest.fixture(name="app") def _app() -> QuartTrio: app = QuartTrio(__name__) @app.route("/", methods=["GET", "POST"]) def index() -> ResponseReturnValue: return request.method @app.route("/gen") def gen() -> ResponseReturnValue: def _gen() -> Generator[bytes, None, None]: yield b"%d" % threading.current_thread().ident for _ in range(2): yield b"b" return _gen(), 200 return app @pytest.mark.trio async def test_sync_request_context(app: QuartTrio) -> None: test_client = app.test_client() response = await test_client.get("/") assert b"GET" in (await response.get_data()) response = await test_client.post("/") assert b"POST" in (await response.get_data()) @pytest.mark.trio async def test_sync_generator(app: QuartTrio) -> None: test_client = app.test_client() response = await test_client.get("/gen") result = await response.get_data() assert result[-2:] == b"bb" assert int(result[:-2]) != threading.current_thread().ident quart_trio-0.12.0/PKG-INFO0000644000000000000000000000754200000000000011757 0ustar00Metadata-Version: 2.1 Name: quart-trio Version: 0.12.0 Summary: A Quart extension to provide trio support Author-Email: pgjones License: MIT Classifier: Development Status :: 3 - Alpha Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content Classifier: Topic :: Software Development :: Libraries :: Python Modules Requires-Python: >=3.9 Requires-Dist: exceptiongroup>=1.1.0; python_version < "3.11" Requires-Dist: hypercorn[trio]>=0.12.0 Requires-Dist: quart>=0.19 Requires-Dist: trio>=0.19.0 Provides-Extra: docs Requires-Dist: pydata_sphinx_theme; extra == "docs" Description-Content-Type: text/x-rst Quart-Trio ========== |Build Status| |docs| |pypi| |python| |license| Quart-Trio is an extension for `Quart `__ to support the `Trio `_ event loop. This is an alternative to using the asyncio event loop present in the Python standard library and supported by default in Quart. Quickstart ---------- QuartTrio can be installed via `pip `_, .. code-block:: console $ pip install quart-trio and requires Python 3.8 or higher (see `python version support `_ for reasoning). A minimal Quart example is, .. code-block:: python from quart import websocket from quart_trio import QuartTrio app = QuartTrio(__name__) @app.route('/') async def hello(): return 'hello' @app.websocket('/ws') async def ws(): while True: await websocket.send('hello') app.run() if the above is in a file called ``app.py`` it can be run as, .. code-block:: console $ python app.py To deploy in a production setting see the `deployment `_ documentation. Contributing ------------ Quart-Trio is developed on `GitHub `_. You are very welcome to open `issues `_ or propose `merge requests `_. Testing ~~~~~~~ The best way to test Quart-Trio is with Tox, .. code-block:: console $ pip install tox $ tox this will check the code style and run the tests. Help ---- The `Quart-Trio `__ and `Quart `__ documentation are the best places to start, after that try searching `stack overflow `_, if you still can't find an answer please `open an issue `_. .. |Build Status| image:: https://github.com/pgjones/quart-trio/actions/workflows/ci.yml/badge.svg :target: https://github.com/pgjones/quart-trio/commits/main .. |docs| image:: https://img.shields.io/badge/docs-passing-brightgreen.svg :target: https://quart-trio.readthedocs.io .. |pypi| image:: https://img.shields.io/pypi/v/quart-trio.svg :target: https://pypi.python.org/pypi/Quart-Trio/ .. |python| image:: https://img.shields.io/pypi/pyversions/quart-trio.svg :target: https://pypi.python.org/pypi/Quart-Trio/ .. |license| image:: https://img.shields.io/badge/license-MIT-blue.svg :target: https://github.com/pgjones/quart-trio/blob/main/LICENSE