diff --git a/example.py b/example.py index c06663d..7416880 100644 --- a/example.py +++ b/example.py @@ -1,4 +1,5 @@ from spiderweb import WebServer +from spiderweb.exceptions import ServerError from spiderweb.response import HttpResponse, JsonResponse, TemplateResponse, RedirectResponse @@ -30,7 +31,7 @@ def json(request): @app.route("/error") def error(request): - return HttpResponse(status_code=500, body="Internal Server Error") + raise ServerError @app.route("/middleware") diff --git a/pyproject.toml b/pyproject.toml index c5323d9..7fdd4a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "spiderweb" -version = "0.4.0" +version = "0.5.0" description = "A small web framework, just big enough to hold your average spider." authors = ["Joe Kaufeld "] readme = "README.md" diff --git a/spiderweb/exceptions.py b/spiderweb/exceptions.py index f1252eb..e3fcd9a 100644 --- a/spiderweb/exceptions.py +++ b/spiderweb/exceptions.py @@ -1,6 +1,7 @@ class SpiderwebException(Exception): # parent error class; all child exceptions should inherit from this - pass + def __str__(self): + return f"{self.__class__.__name__}({self.code}, {self.msg})" class SpiderwebNetworkException(SpiderwebException): @@ -11,14 +12,46 @@ class SpiderwebNetworkException(SpiderwebException): self.msg = msg self.desc = desc - def __str__(self): - return f"{self.__class__.__name__}({self.code}, {self.msg})" - class APIError(SpiderwebNetworkException): pass +class NotFound(SpiderwebNetworkException): + def __init__(self): + self.code = 404 + self.msg = "Not Found" + self.desc = "The requested resource could not be found" + + +class BadRequest(SpiderwebNetworkException): + def __init__(self, desc=None): + self.code = 400 + self.msg = "Bad Request" + self.desc = desc if desc else "The request could not be understood by the server" + + +class Unauthorized(SpiderwebNetworkException): + def __init__(self, desc=None): + self.code = 401 + self.msg = "Unauthorized" + self.desc = desc if desc else "The request requires user authentication" + + +class Forbidden(SpiderwebNetworkException): + def __init__(self, desc=None): + self.code = 403 + self.msg = "Forbidden" + self.desc = desc if desc else "You are not allowed to access this resource" + + +class ServerError(SpiderwebNetworkException): + def __init__(self, desc=None): + self.code = 500 + self.msg = "Internal Server Error" + self.desc = desc if desc else "The server has encountered an error" + + class ConfigError(SpiderwebException): pass diff --git a/spiderweb/main.py b/spiderweb/main.py index 7d89ad5..c9fb277 100644 --- a/spiderweb/main.py +++ b/spiderweb/main.py @@ -5,7 +5,6 @@ import json import re import signal -import sys import time import traceback from http.server import BaseHTTPRequestHandler, HTTPServer @@ -23,7 +22,7 @@ from spiderweb.exceptions import ( ConfigError, ParseError, GeneralException, - NoResponseError, UnusedMiddleware, + NoResponseError, UnusedMiddleware, SpiderwebNetworkException, NotFound, ) from spiderweb.request import Request from spiderweb.response import HttpResponse, JsonResponse, TemplateResponse, RedirectResponse @@ -125,7 +124,7 @@ class WebServer(HTTPServer): def convert_path(self, path: str): """Convert a path to a regex.""" - parts = path.split("/") + parts = path.split("/") for i, part in enumerate(parts): if part.startswith("<") and part.endswith(">"): name = part[1:-1] @@ -142,11 +141,8 @@ class WebServer(HTTPServer): raise ParseError(f"Unknown converter {converter}") else: converter = StrConverter # noqa: F405 - parts[i] = r"(?P<%s>%s)" % ( - f"{name}__{str(converter.__name__)}", - converter.regex, - ) - return re.compile(r"^%s$" % "/".join(parts)) + parts[i] = rf"(?P<{name}__{str(converter.__name__)}>{converter.regex})" + return re.compile(rf"^{'/'.join(parts)}$") def check_for_route_duplicates(self, path: str): if self.convert_path(path) in self.handler_class._routes: @@ -233,6 +229,9 @@ class WebServer(HTTPServer): class RequestHandler(BaseHTTPRequestHandler): # I can't help the naming convention of these because that's what # BaseHTTPRequestHandler uses for some weird reason + + # These stop pycharm from complaining about these not existing. They're + # injected by the WebServer class at runtime _routes = {} middleware = [] @@ -269,7 +268,7 @@ class RequestHandler(BaseHTTPRequestHandler): return self._routes[option], convert_match_to_dict( match_data.groupdict() ) - raise APIError(404, "No route found") + raise NotFound() def get_error_route(self, code: int) -> Callable: try: @@ -319,10 +318,10 @@ class RequestHandler(BaseHTTPRequestHandler): self.middleware.remove(middleware) continue - def prepare_response(self, request, resp) -> HttpResponse: + def prepare_and_fire_response(self, request, resp) -> None: try: if isinstance(resp, dict): - self.fire_response(JsonResponse(data=resp)) + self.fire_response(request, JsonResponse(data=resp)) if isinstance(resp, TemplateResponse): if hasattr(self, "env"): # injected from above resp.set_template_loader(self.env) @@ -335,17 +334,25 @@ class RequestHandler(BaseHTTPRequestHandler): except APIError: raise - except ConnectionAbortedError as e: - log.error(f"GET {self.path} : {e}") except Exception: log.error(traceback.format_exc()) self.fire_response(request, self.get_error_route(500)(request)) + def send_error_response(self, request: Request, e: SpiderwebNetworkException): + try: + self.send_error(e.code, e.msg, e.desc) + except ConnectionAbortedError as e: + log.error(f"{request.method} {self.path} : {e}") + def handle_request(self, request): request.url = urlparse.urlparse(request.path) - handler, additional_args = self.get_route(request.url.path) + try: + handler, additional_args = self.get_route(request.url.path) + except NotFound: + handler = self.get_error_route(404) + additional_args = {} if request.url.query: params = urlparse.parse_qs(request.url.query) @@ -363,18 +370,9 @@ class RequestHandler(BaseHTTPRequestHandler): resp = handler(request, **additional_args) if resp is None: raise NoResponseError(f"View {handler} returned None.") - if isinstance(resp, dict): - self.fire_response(request, JsonResponse(data=resp)) - if isinstance(resp, TemplateResponse): - if hasattr(self, "env"): # injected from above - resp.set_template_loader(self.env) - - self.process_response_middleware(request, resp) - self.fire_response(request, resp) + # run the response through the middleware and send it + self.prepare_and_fire_response(request, resp) else: - raise APIError(404) - except APIError as e: - try: - self.send_error(e.code, e.msg, e.desc) - except ConnectionAbortedError as e: - log.error(f"GET {self.path} : {e}") + raise SpiderwebNetworkException(404) + except SpiderwebNetworkException as e: + self.send_error_response(request, e)