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
|
||||
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
|
||||
|
||||
@@ -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', '')
|
||||
|
||||
Reference in New Issue
Block a user