diff --git a/src/turbosloth/exceptions/__init__.py b/src/turbosloth/exceptions/__init__.py new file mode 100644 index 0000000..159842c --- /dev/null +++ b/src/turbosloth/exceptions/__init__.py @@ -0,0 +1,2 @@ +from .client_errors import * +from .server_errors import * diff --git a/src/turbosloth/exceptions/client_errors.py b/src/turbosloth/exceptions/client_errors.py new file mode 100644 index 0000000..d028d5f --- /dev/null +++ b/src/turbosloth/exceptions/client_errors.py @@ -0,0 +1,265 @@ +from turbosloth.exceptions.http_base import HTTPException + +_client_error_codes = { + 400: 'Bad Request', + 401: 'Unauthorized', + 402: 'Payment Required', + 403: 'Forbidden', + 404: 'Not Found', + 405: 'Method Not Allowed', + 406: 'Not Acceptable', + 407: 'Proxy Authentication Required', + 408: 'Request Timeout', + 409: 'Conflict', + 410: 'Gone', + 411: 'Length Required', + 412: 'Precondition Failed', + 413: 'Content Too Large', + 414: 'URI Too Long', + 415: 'Unsupported Media Type', + 416: 'Range Not Satisfiable', + 417: 'Expectation Failed', + 418: 'I\'m a teapot', + 421: 'Misdirected Request', + 422: 'Unprocessable Content', + 423: 'Locked', + 424: 'Failed Dependency', + 425: 'Too Early Experimental', + 426: 'Upgrade Required', + 428: 'Precondition Required', + 429: 'Too Many Requests', + 431: 'Request Header Fields Too Large', + 451: 'Unavailable For Legal Reasons', +} + + +class BadRequestException(HTTPException): + code = 400 + description = _client_error_codes[400] + + def __init__(self, message: str | None = None): + super().__init__(self.code, self.description, message) + + +class UnauthorizedException(HTTPException): + code = 401 + description = _client_error_codes[401] + + def __init__(self, message: str | None = None): + super().__init__(self.code, self.description, message) + + +class PaymentRequiredException(HTTPException): + code = 402 + description = _client_error_codes[402] + + def __init__(self, message: str | None = None): + super().__init__(self.code, self.description, message) + + +class ForbiddenException(HTTPException): + code = 403 + description = _client_error_codes[403] + + def __init__(self, message: str | None = None): + super().__init__(self.code, self.description, message) + + +class NotFoundException(HTTPException): + code = 404 + description = _client_error_codes[404] + + def __init__(self, message: str | None = None): + super().__init__(self.code, self.description, message) + + +class MethodNotAllowedException(HTTPException): + code = 405 + description = _client_error_codes[405] + + def __init__(self, message: str | None = None): + super().__init__(self.code, self.description, message) + + +class NotAcceptableException(HTTPException): + code = 406 + description = _client_error_codes[406] + + def __init__(self, message: str | None = None): + super().__init__(self.code, self.description, message) + + +class ProxyAuthenticationRequiredException(HTTPException): + code = 407 + description = _client_error_codes[407] + + def __init__(self, message: str | None = None): + super().__init__(self.code, self.description, message) + + +class RequestTimeoutException(HTTPException): + code = 408 + description = _client_error_codes[408] + + def __init__(self, message: str | None = None): + super().__init__(self.code, self.description, message) + + +class ConflictException(HTTPException): + code = 409 + description = _client_error_codes[409] + + def __init__(self, message: str | None = None): + super().__init__(self.code, self.description, message) + + +class GoneException(HTTPException): + code = 410 + description = _client_error_codes[410] + + def __init__(self, message: str | None = None): + super().__init__(self.code, self.description, message) + + +class LengthRequiredException(HTTPException): + code = 411 + description = _client_error_codes[411] + + def __init__(self, message: str | None = None): + super().__init__(self.code, self.description, message) + + +class PreconditionFailedException(HTTPException): + code = 412 + description = _client_error_codes[412] + + def __init__(self, message: str | None = None): + super().__init__(self.code, self.description, message) + + +class ContentTooLargeException(HTTPException): + code = 413 + description = _client_error_codes[413] + + def __init__(self, message: str | None = None): + super().__init__(self.code, self.description, message) + + +class URITooLongException(HTTPException): + code = 414 + description = _client_error_codes[414] + + def __init__(self, message: str | None = None): + super().__init__(self.code, self.description, message) + + +class UnsupportedMediaTypeException(HTTPException): + code = 415 + description = _client_error_codes[415] + + def __init__(self, message: str | None = None): + super().__init__(self.code, self.description, message) + + +class RangeNotSatisfiableException(HTTPException): + code = 416 + description = _client_error_codes[416] + + def __init__(self, message: str | None = None): + super().__init__(self.code, self.description, message) + + +class ExpectationFailedException(HTTPException): + code = 417 + description = _client_error_codes[417] + + def __init__(self, message: str | None = None): + super().__init__(self.code, self.description, message) + + +class ImaTeapotException(HTTPException): + code = 418 + description = _client_error_codes[418] + + def __init__(self, message: str | None = None): + super().__init__(self.code, self.description, message) + + +class MisdirectedRequestException(HTTPException): + code = 421 + description = _client_error_codes[421] + + def __init__(self, message: str | None = None): + super().__init__(self.code, self.description, message) + + +class UnprocessableContentException(HTTPException): + code = 422 + description = _client_error_codes[422] + + def __init__(self, message: str | None = None): + super().__init__(self.code, self.description, message) + + +class LockedException(HTTPException): + code = 423 + description = _client_error_codes[423] + + def __init__(self, message: str | None = None): + super().__init__(self.code, self.description, message) + + +class FailedDependencyException(HTTPException): + code = 424 + description = _client_error_codes[424] + + def __init__(self, message: str | None = None): + super().__init__(self.code, self.description, message) + + +class TooEarlyExperimentalException(HTTPException): + code = 425 + description = _client_error_codes[425] + + def __init__(self, message: str | None = None): + super().__init__(self.code, self.description, message) + + +class UpgradeRequiredException(HTTPException): + code = 426 + description = _client_error_codes[426] + + def __init__(self, message: str | None = None): + super().__init__(self.code, self.description, message) + + +class PreconditionRequiredException(HTTPException): + code = 428 + description = _client_error_codes[428] + + def __init__(self, message: str | None = None): + super().__init__(self.code, self.description, message) + + +class TooManyRequestsException(HTTPException): + code = 429 + description = _client_error_codes[429] + + def __init__(self, message: str | None = None): + super().__init__(self.code, self.description, message) + + +class RequestHeaderFieldsTooLargeException(HTTPException): + code = 431 + description = _client_error_codes[431] + + def __init__(self, message: str | None = None): + super().__init__(self.code, self.description, message) + + +class UnavailableForLegalReasonsException(HTTPException): + code = 451 + description = _client_error_codes[451] + + def __init__(self, message: str | None = None): + super().__init__(self.code, self.description, message) diff --git a/src/turbosloth/exceptions/http_base.py b/src/turbosloth/exceptions/http_base.py new file mode 100644 index 0000000..62cbc62 --- /dev/null +++ b/src/turbosloth/exceptions/http_base.py @@ -0,0 +1,19 @@ +from typing import Callable, Optional + + +class HTTPException(Exception): + def __init__(self, code: int, description: str, message: str | None = None): + m = f'{code}\t{description}' + if message is not None: + m += f': {message}' + super().__init__(m) + self.code = code + self.description = description + self.message = message + + +def AutoException(code: int, description: str) -> Callable[[Optional[str]], HTTPException]: + def foo(message: str | None = None): + return HTTPException(code, description, message) + + return foo diff --git a/src/turbosloth/exceptions/server_errors.py b/src/turbosloth/exceptions/server_errors.py new file mode 100644 index 0000000..eb11552 --- /dev/null +++ b/src/turbosloth/exceptions/server_errors.py @@ -0,0 +1,103 @@ +from turbosloth.exceptions import HTTPException + +_server_error_codes = { + 500: 'Internal Server Error', + 501: 'Not Implemented', + 502: 'Bad Gateway', + 503: 'Service Unavailable', + 504: 'Gateway Timeout', + 505: 'HTTP Version Not Supported', + 506: 'Variant Also Negotiates', + 507: 'Insufficient Storage', + 508: 'Loop Detected', + 510: 'Not Extended', + 511: 'Network Authentication Required', +} + + +class InternalServerError(HTTPException): + code = 500 + description = _server_error_codes[500] + + def __init__(self, message: str | None = None): + super().__init__(self.code, self.description, message) + + +class NotImplementedException(HTTPException): + code = 501 + description = _server_error_codes[501] + + def __init__(self, message: str | None = None): + super().__init__(self.code, self.description, message) + + +class BadGatewayException(HTTPException): + code = 502 + description = _server_error_codes[502] + + def __init__(self, message: str | None = None): + super().__init__(self.code, self.description, message) + + +class ServiceUnavailableException(HTTPException): + code = 503 + description = _server_error_codes[503] + + def __init__(self, message: str | None = None): + super().__init__(self.code, self.description, message) + + +class GatewayTimeoutException(HTTPException): + code = 504 + description = _server_error_codes[504] + + def __init__(self, message: str | None = None): + super().__init__(self.code, self.description, message) + + +class HTTPVersionNotSupportedException(HTTPException): + code = 505 + description = _server_error_codes[505] + + def __init__(self, message: str | None = None): + super().__init__(self.code, self.description, message) + + +class VariantAlsoNegotiatesException(HTTPException): + code = 506 + description = _server_error_codes[506] + + def __init__(self, message: str | None = None): + super().__init__(self.code, self.description, message) + + +class InsufficientStorageException(HTTPException): + code = 507 + description = _server_error_codes[507] + + def __init__(self, message: str | None = None): + super().__init__(self.code, self.description, message) + + +class LoopDetectedException(HTTPException): + code = 508 + description = _server_error_codes[508] + + def __init__(self, message: str | None = None): + super().__init__(self.code, self.description, message) + + +class NotExtendedException(HTTPException): + code = 510 + description = _server_error_codes[510] + + def __init__(self, message: str | None = None): + super().__init__(self.code, self.description, message) + + +class NetworkAuthenticationRequiredException(HTTPException): + code = 511 + description = _server_error_codes[511] + + def __init__(self, message: str | None = None): + super().__init__(self.code, self.description, message) diff --git a/src/turbosloth/router.py b/src/turbosloth/router.py index 01980cc..b7ab876 100644 --- a/src/turbosloth/router.py +++ b/src/turbosloth/router.py @@ -3,6 +3,7 @@ from __future__ import annotations import typing from typing import Optional, Sequence, get_args +from .exceptions import MethodNotAllowedException, NotFoundException, HTTPException from .types import HandlerType, MethodType @@ -30,12 +31,11 @@ class Route: if len(sequence) == 0: ret = self.handler.get(method) if ret is None: - # TODO: extract exceptions - raise ValueError('405') + raise MethodNotAllowedException(', '.join(map(str, self.handler.keys()))) return ret subroute = self.static_subroutes.get(sequence[0]) if subroute is None: - raise ValueError('404') + raise NotFoundException('/'.join(sequence)) return subroute.get(method, sequence[1:]) @@ -59,6 +59,12 @@ class Router: while len(segments) > 0 and len(segments[0]) == 0: segments = segments[1:] - - h = self._root.get(method, segments) + try: + h = self._root.get(method, segments) + except NotFoundException as e: + raise NotFoundException(path or '/') from e + except MethodNotAllowedException as e: + raise MethodNotAllowedException( + f'{method} /{path}' + ', allowed: ' + (e.message if len(e.message) > 0 else 'none')) \ + from e return h diff --git a/tests/test_router.py b/tests/test_router.py index 6367499..887d0e7 100644 --- a/tests/test_router.py +++ b/tests/test_router.py @@ -1,6 +1,7 @@ import pytest from src.turbosloth.router import Router +from src.turbosloth.exceptions import NotFoundException, MethodNotAllowedException def test_router_root_handler(): @@ -46,7 +47,10 @@ def test_router_match(): r = Router() r.add('GET', 'asdf', f) assert r.match('GET', '/asdf') - with pytest.raises(ValueError, match='404'): + with pytest.raises(NotFoundException, match='404\tNot Found: asd'): r.match('GET', 'asd') - with pytest.raises(ValueError, match='405'): + with pytest.raises(MethodNotAllowedException, match=f'405\tMethod Not Allowed: POST /asdf, allowed: GET'): r.match('POST', 'asdf') + + with pytest.raises(MethodNotAllowedException, match=f'405\tMethod Not Allowed: POST /, allowed: none'): + r.match('POST', '')