~tsileo/gemapi

ab3f45fe67ca7baf03c3986b0b284b4f416265cd — Thomas Sileo a month ago 5d63e74 main
Simplify response handling
2 files changed, 73 insertions(+), 46 deletions(-)

M gemapi/applications.py
M gemapi/responses.py
M gemapi/applications.py => gemapi/applications.py +52 -39
@@ 7,9 7,10 @@ from loguru import logger
from gemapi.request import Input
from gemapi.request import Request
from gemapi.request import SensitiveInput
from gemapi.responses import BadRequestError
from gemapi.responses import BadRequestResponse
from gemapi.responses import InputResponse
from gemapi.responses import NotFoundResponse
from gemapi.responses import NotFoundError
from gemapi.responses import Response
from gemapi.responses import SensitiveInputResponse
from gemapi.responses import StatusError


@@ 49,21 50,24 @@ class Application:
            # 'gemini://localhost/\r\n'
            # 1. ending with a <CR><LF>
            if not data.endswith(b"\r\n"):
                raise ValueError("Not ending with a CRLF")
                raise BadRequestError("Not ending with a CRLF")

            message = data.decode()

            parsed_url = urlparse(message[:-2])

            if parsed_url.scheme != "gemini":
                raise ValueError("Not a gemini URL")
                raise BadRequestError(f"Invalid scheme {parsed_url.scheme}")

            if parsed_url.path == "":
                parsed_url = parsed_url._replace(path="/")

            if "." in parsed_url.path:
                raise ValueError("dots in path are not allowed")
                raise BadRequestError("dots in path are not allowed")

        except StatusError as status_error:
            logger.error(f"{client_host}:{client_port} - {status_error.data()}")
            resp = status_error.as_response()
        except Exception:
            logger.exception(f"{client_host}:{client_port} - 51")
            resp = BadRequestResponse("Bad request")


@@ 78,15 82,24 @@ class Application:
            try:
                resp = await self._process_request(req)
            except StatusError as status_error:
                logger.error(
                    f"{client_host}:{client_port} - {req.parsed_url.geturl()} "
                    f"{status_error.data()}"
                )
                resp = status_error.as_response()
            except Exception:
                logger.exception(f"{client_host}:{client_port} - 40")
                resp = TemporaryFailureResponse("Failed to process request")

            logger.info(
                f"{client_host}:{client_port} - "
                f"{req.parsed_url.geturl()} {resp.status_code}"
            )
                logger.exception(
                    f"{client_host}:{client_port} - "
                    f"{req.parsed_url.geturl()} {resp.status_code.name} "
                    f"{resp.status_code.value} {resp.meta}"
                )
            else:
                logger.info(
                    f"{client_host}:{client_port} - "
                    f"{req.parsed_url.geturl()} {resp.status_code.name} "
                    f"{resp.status_code.value} {resp.meta}"
                )

        writer.write(resp.as_bytes())
        await writer.drain()


@@ 111,36 124,36 @@ class Application:

        # Build the response
        if not matched_route:
            resp = NotFoundResponse()
            raise NotFoundError("Not found")

        if matched_params is None:
            raise ValueError("Missing matched params")

        handler_params: dict[str, Any] = {}
        handler_params.update(matched_params)
        if matched_route.input_parameter and not req.parsed_url.query:
            if matched_route.input_parameter.annotation is Input:
                resp = InputResponse(
                    matched_route.input_parameter.name,
                )
            elif matched_route.input_parameter.annotation is SensitiveInput:
                resp = SensitiveInputResponse(
                    matched_route.input_parameter.name,
                )
            else:
                raise ValueError(
                    "Unexpected input param type "
                    f"{matched_route.input_parameter.annotation}"
                )
        else:
            if matched_params is None:
                raise ValueError("Missing matched params")

            handler_params: dict[str, Any] = {}
            handler_params.update(matched_params)
            if matched_route.input_parameter and not req.parsed_url.query:
                if matched_route.input_parameter.annotation is Input:
                    resp = InputResponse(
                        matched_route.input_parameter.name,
                    )
                elif matched_route.input_parameter.annotation is SensitiveInput:
                    resp = SensitiveInputResponse(
                        matched_route.input_parameter.name,
                    )
                else:
                    raise ValueError(
                        "Unexpected input param type "
                        f"{matched_route.input_parameter.annotation}"
                    )
            if matched_route.input_parameter:
                handler_params[matched_route.input_parameter.name] = Input(
                    req.parsed_url.query
                )
            # TODO: pass the path params wit the right type as kwargs
            if matched_route.handler_is_coroutine:
                resp = await matched_route.handler(req, **handler_params)
            else:
                if matched_route.input_parameter:
                    handler_params[matched_route.input_parameter.name] = Input(
                        req.parsed_url.query
                    )
                # TODO: pass the path params wit the right type as kwargs
                if matched_route.handler_is_coroutine:
                    resp = await matched_route.handler(req, **handler_params)
                else:
                    resp = matched_route.handler(req, **handler_params)
                resp = matched_route.handler(req, **handler_params)

        return resp

M gemapi/responses.py => gemapi/responses.py +21 -7
@@ 35,17 35,16 @@ class Response:
        meta: str,
        body: str | None = None,
    ) -> None:
        self.status_code = status_code
        self.status_code = (
            status_code
            if isinstance(status_code, StatusCode)
            else StatusCode(status_code)
        )
        self.meta = meta
        self.body = body

    def as_bytes(self) -> bytes:
        status_code = (
            self.status_code.value
            if isinstance(self.status_code, StatusCode)
            else self.status_code
        )
        data = f"{status_code} {self.meta}\r\n"
        data = f"{self.status_code.value} {self.meta}\r\n"
        if self.body:
            data += self.body



@@ 62,11 61,26 @@ class StatusError(Exception):
    def as_response(self) -> Response:
        return Response(self.STATUS_CODE, self.meta)

    def data(self) -> str:
        return f"{self.STATUS_CODE.name}: {self.STATUS_CODE.value} {self.meta}"


class NotFoundError(StatusError):
    STATUS_CODE = StatusCode.NOT_FOUND


class GoneError(StatusError):
    STATUS_CODE = StatusCode.GONE


class BadRequestError(StatusError):
    STATUS_CODE = StatusCode.BAD_REQUEST


class TemporaryFailureError(StatusError):
    STATUS_CODE = StatusCode.TEMPORARY_FAILURE


class NotFoundResponse(Response):
    def __init__(self, meta: str = "Not found") -> None:
        super().__init__(StatusCode.NOT_FOUND, meta)