async def test_body_validation(): @resource class Resource: @operation async def post(self, a: Annotated[int, InBody]) -> str: return f"{a}" application = Application(Resource()) request = Request(method="POST", path="/", body=BytesStream(b'{"a": "not_int"}')) response = await application(request) assert response.status == http.HTTPStatus.BAD_REQUEST.value
async def test_request_in_body_parameters(): @resource class Resource: @operation async def post(self, a: Annotated[str, InBody], b: Annotated[str, InBody]) -> str: return f"{a}{b}" application = Application(Resource()) request = Request(method="POST", path="/", body=BytesStream(b'{"a": "foo", "b": "bar"}')) response = await application(request) assert response.status == http.HTTPStatus.OK.value assert await body(response) == b"foobar"
async def test_stream_request_body(): @resource class Resource: @operation async def post(self, foo: Annotated[Stream, AsBody]) -> BytesStream: content = b"".join([b async for b in foo]) return BytesStream(content) application = Application(Resource()) content = b"abcdefg" request = Request(method="POST", path="/", body=BytesStream(content)) response = await application(request) assert response.status == http.HTTPStatus.OK.value assert response.headers["Content-Length"] == str(len(content)) assert await body(response) == content
async def simple_error_filter(request: Request): """Generates a simple JSON error response if an exception is raised.""" try: try: yield except fondat.error.Error: raise except Exception as e: _logger.exception("unhandled exception") raise InternalServerError from e except fondat.error.Error as err: body = str(err) response = Response() response.status = err.status response.headers["content-type"] = "text/plain; charset=UTF-8" response.headers["content-length"] = str(len(body)) response.body = BytesStream(body.encode()) yield response
async def test_request_body_dataclass(): @dataclass class Model: a: int b: str @resource class Resource: @operation async def post(self, val: Annotated[Model, AsBody]) -> Model: return val application = Application(Resource()) m = Model(a=1, b="s") codec = get_codec(Binary, Model) request = Request(method="POST", path="/", body=BytesStream(codec.encode(m))) response = await application(request) assert response.status == http.HTTPStatus.OK.value assert codec.decode(await body(response)) == m
async def _handle(self, request: Request) -> Response: if not request.path.startswith(self.path): raise NotFoundError path = request.path[len(self.path):] response = Response() method = request.method.lower() segments = path.split("/") if path else () resource = self.root operation = None for segment in segments: if operation: # cannot have segments after operation name raise NotFoundError try: resource = await _subordinate(resource, segment) except NotFoundError: try: operation = getattr(resource, segment) if not fondat.resource.is_operation(operation): raise NotFoundError except AttributeError: raise NotFoundError if operation: # operation name as segment (@query or @mutation) fondat_op = getattr(operation, "_fondat_operation", None) if not fondat_op or not fondat_op.method == method: raise MethodNotAllowedError else: # no remaining segments; operation name as HTTP method operation = getattr(resource, method, None) if not fondat.resource.is_operation(operation): raise MethodNotAllowedError body = await _decode_body(operation, request) params = {} signature = inspect.signature(operation) hints = typing.get_type_hints(operation, include_extras=True) return_hint = hints.get("return", type(None)) for name, hint in hints.items(): if name == "return": continue required = signature.parameters[ name].default is inspect.Parameter.empty param_in = get_param_in(operation, name, hint) if isinstance(param_in, AsBody) and body is not None: params[name] = body elif isinstance(param_in, InBody) and body is not None: if param_in.name in body: params[name] = body[param_in.name] elif isinstance(param_in, InQuery): if param_in.name in request.query: codec = get_codec(String, hint) try: with DecodeError.path_on_error(param_in.name): params[name] = codec.decode( request.query[param_in.name]) except DecodeError as de: raise BadRequestError from de if name not in params and required: if not is_optional(hint): raise BadRequestError from DecodeError( "required parameter", ["«params»", name]) params[name] = None result = await operation(**params) if not is_subclass(return_hint, Stream): return_codec = get_codec(Binary, return_hint) try: result = BytesStream(return_codec.encode(result), return_codec.content_type) except Exception as e: raise InternalServerError from e response.body = result response.headers["Content-Type"] = response.body.content_type if response.body.content_length is not None: if response.body.content_length == 0: response.status = http.HTTPStatus.NO_CONTENT.value else: response.headers["Content-Length"] = str( response.body.content_length) return response
async def test_bytes_stream(): value = b"hello" assert await _ajoin(BytesStream(value)) == value
async def post(self, foo: Annotated[Stream, AsBody]) -> BytesStream: content = b"".join([b async for b in foo]) return BytesStream(content)
async def get(self) -> Stream: return BytesStream(b"12345")