diff --git a/app/common.py b/app/common.py new file mode 100644 index 0000000..d56bbf2 --- /dev/null +++ b/app/common.py @@ -0,0 +1,17 @@ +import secrets + +from fastapi.templating import Jinja2Templates +from pydantic import BaseSettings + + +class Settings(BaseSettings): + secret_key: str = secrets.token_hex(32) + templates_dir: str = '../templates' + static_dir: str = '../static' + +settings = Settings() + + +templates = Jinja2Templates( + directory=settings.templates_dir, +) diff --git a/app/errors.py b/app/errors.py new file mode 100644 index 0000000..402ba91 --- /dev/null +++ b/app/errors.py @@ -0,0 +1,50 @@ +"""Custom error pages for FastAPI app""" + +from pathlib import Path + +from fastapi import Request, Response +from fastapi import HTTPException + +from . import paths +from . import respond +from .common import settings + +# Add other HTTP error codes +codes = [404, 500] + + +class ErrorsPaths(paths.Paths): + """Sets up custom error pages, + inherited from paths.Paths""" + + def add_paths(self) -> None: + + for code in codes: + self.add_handler(code) + + def add_handler(self, code: int) -> None: + """Adds an error handler to FastAPI app. + Only for internal use! + + Args: + code (int): HTTP error code + """ + + tmpl_dir = ( + Path(__file__).parent / + settings.templates_dir + ) + file = tmpl_dir / f'{code}.html' + + if not file.exists(): + return + + @self.app.exception_handler(code) + async def handler(req: Request, exc: HTTPException) -> Response: + + return respond.with_tmpl( + f'{code}.html', + code=code, + request=req, + exc=exc, + ) diff --git a/app/main.py b/app/main.py index 9100756..52c9a7f 100644 --- a/app/main.py +++ b/app/main.py @@ -1,16 +1,9 @@ -import secrets - from fastapi import FastAPI from fastapi.staticfiles import StaticFiles -from pydantic import BaseSettings +#from .common import settings, templates -class Settings(BaseSettings): - secret_key: str = secrets.token_hex(32) - - -settings = Settings() app = FastAPI() app.mount( '/static', diff --git a/app/pages.py b/app/pages.py new file mode 100644 index 0000000..5a41aff --- /dev/null +++ b/app/pages.py @@ -0,0 +1,16 @@ +"""Main FastAPI paths""" + +from fastapi import Response + +from . import paths +from . import respond + + +class MainPaths(paths.Paths): + """Main FastAPI app paths, inherits paths.Paths""" + + def add_paths(self) -> None: + + @self.app.get('/') + def index() -> Response: + return respond.with_tmpl('index.html') diff --git a/app/paths.py b/app/paths.py new file mode 100644 index 0000000..82670ae --- /dev/null +++ b/app/paths.py @@ -0,0 +1,21 @@ +import abc + +from fastapi import FastAPI + + +class Paths(abc.ABC): + """Abstract class for storing paths for FastAPI app""" + + def __init__(self, app: FastAPI) -> None: + """Abstract class for storing paths + for FastAPI app + + Args: + app (FastAPI): Application object + """ + + self.app = app + + @abc.abstractmethod + def add_paths(self) -> None: + """Add paths to the FastAPI application""" diff --git a/app/respond.py b/app/respond.py new file mode 100644 index 0000000..279969f --- /dev/null +++ b/app/respond.py @@ -0,0 +1,77 @@ +import os +import mimetypes +from typing import Optional, Mapping + +from fastapi import Response +from fastapi.responses import FileResponse +from starlette.background import BackgroundTask + +from .common import templates + + +def with_tmpl( + name: str, + code: int = 200, + headers: Optional[Mapping[str, str]] = None, + background: Optional[BackgroundTask] = None, + **context) -> Response: + """Render a Jinja2 template and return Response object. + `response_class` parameter is not needed + + Args: + name (str): Template filename + code (int, optional): HTTP response code + headers (Optional[Mapping[str, str]], optional): + Additional headers, passed to Response constructor + background (Optional[BackgroundTask], optional): + Background task, passed to Response constructor + + Returns: + FastAPI's TemplateResponse object + """ + + return templates.TemplateResponse( + name=name, + context=context, + status_code=code, + headers=headers, + background=background, + ) + + +def with_file( + path: os.PathLike, + mime: Optional[str] = None, + code: int = 200, + headers: Optional[Mapping[str, str]] = None, + background: Optional[BackgroundTask] = None) -> FileResponse: + """Send a file specified in `path` + automatically guessing mimetype if `mime` is None + + Args: + path (os.PathLike): File path + mime (Optional[str], optional): File mimetype + code (int, optional): HTTP response code + headers (Optional[Mapping[str, str]], optional): + Additional headers, passed to Response constructor + background (Optional[BackgroundTask], optional): + Background task, passed to Response constructor + + Returns: + FileResponse: FastAPI's FileResponse object + """ + + return FileResponse( + path=path, + media_type=( + mime or + mimetypes.guess_type(path)[0] + ), + status_code=code, + headers=headers, + background=background, + ) + + +# Alias +with_template = with_tmpl