Add 4xx and 5xx http-code exception wrappers
This commit is contained in:
2
src/turbosloth/exceptions/__init__.py
Normal file
2
src/turbosloth/exceptions/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from .client_errors import *
|
||||||
|
from .server_errors import *
|
||||||
265
src/turbosloth/exceptions/client_errors.py
Normal file
265
src/turbosloth/exceptions/client_errors.py
Normal file
@@ -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)
|
||||||
19
src/turbosloth/exceptions/http_base.py
Normal file
19
src/turbosloth/exceptions/http_base.py
Normal file
@@ -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
|
||||||
103
src/turbosloth/exceptions/server_errors.py
Normal file
103
src/turbosloth/exceptions/server_errors.py
Normal file
@@ -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)
|
||||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|||||||
import typing
|
import typing
|
||||||
from typing import Optional, Sequence, get_args
|
from typing import Optional, Sequence, get_args
|
||||||
|
|
||||||
|
from .exceptions import MethodNotAllowedException, NotFoundException, HTTPException
|
||||||
from .types import HandlerType, MethodType
|
from .types import HandlerType, MethodType
|
||||||
|
|
||||||
|
|
||||||
@@ -30,12 +31,11 @@ class Route:
|
|||||||
if len(sequence) == 0:
|
if len(sequence) == 0:
|
||||||
ret = self.handler.get(method)
|
ret = self.handler.get(method)
|
||||||
if ret is None:
|
if ret is None:
|
||||||
# TODO: extract exceptions
|
raise MethodNotAllowedException(', '.join(map(str, self.handler.keys())))
|
||||||
raise ValueError('405')
|
|
||||||
return ret
|
return ret
|
||||||
subroute = self.static_subroutes.get(sequence[0])
|
subroute = self.static_subroutes.get(sequence[0])
|
||||||
if subroute is None:
|
if subroute is None:
|
||||||
raise ValueError('404')
|
raise NotFoundException('/'.join(sequence))
|
||||||
return subroute.get(method, sequence[1:])
|
return subroute.get(method, sequence[1:])
|
||||||
|
|
||||||
|
|
||||||
@@ -59,6 +59,12 @@ class Router:
|
|||||||
|
|
||||||
while len(segments) > 0 and len(segments[0]) == 0:
|
while len(segments) > 0 and len(segments[0]) == 0:
|
||||||
segments = segments[1:]
|
segments = segments[1:]
|
||||||
|
try:
|
||||||
h = self._root.get(method, segments)
|
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
|
return h
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from src.turbosloth.router import Router
|
from src.turbosloth.router import Router
|
||||||
|
from src.turbosloth.exceptions import NotFoundException, MethodNotAllowedException
|
||||||
|
|
||||||
|
|
||||||
def test_router_root_handler():
|
def test_router_root_handler():
|
||||||
@@ -46,7 +47,10 @@ def test_router_match():
|
|||||||
r = Router()
|
r = Router()
|
||||||
r.add('GET', 'asdf', f)
|
r.add('GET', 'asdf', f)
|
||||||
assert r.match('GET', '/asdf')
|
assert r.match('GET', '/asdf')
|
||||||
with pytest.raises(ValueError, match='404'):
|
with pytest.raises(NotFoundException, match='404\tNot Found: asd'):
|
||||||
r.match('GET', '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')
|
r.match('POST', 'asdf')
|
||||||
|
|
||||||
|
with pytest.raises(MethodNotAllowedException, match=f'405\tMethod Not Allowed: POST /, allowed: none'):
|
||||||
|
r.match('POST', '')
|
||||||
|
|||||||
Reference in New Issue
Block a user