diff --git a/.env_debug b/.env_debug index 1342d7c..1e3e50b 100644 --- a/.env_debug +++ b/.env_debug @@ -1,3 +1,5 @@ +SESSION_KEY=debug +CSRF_KEY=debug DB_HOST=localhost DB_PORT=3306 DB_USER=darkcat09 diff --git a/README.md b/README.md index a614eae..b304160 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,238 @@ -# tmpl-fastapi +# FastAPI template +It is a simple and handy FastAPI template +for combined frontend and backend as in Flask. +Includes Jinja, WTForms, MySQL ORM and Docker. -Simple and handy FastAPI template for combined frontend and backend as in Flask, includes Jinja, WTForms, MySQL ORM and Docker \ No newline at end of file + +## Features + - Basic FastAPI app + - `@app.route`s are defined in separate files + - Lots of helper functions + - Jinja2 templates, WTForms + - MySQL/MariaDB database, SQLAlchemy 2 + - AutoPEP8 formatter, MyPy and Pylint + - Dockerfile, Docker-Compose + + +## Structure + +### Configuration +#### `.env`s + - `.env_debug` contains the DB configuration and tokens + for the development environment, must not be used in production + - `.env` is a config for the application itself, + loaded only in docker-compose by default + - `.env_db` is a config for MySQL/MariaDB server, + also loaded only in docker-compose for the mariadb container +#### The main config loaded by `app/common.py`: + - `templates_dir`, `static_dir` contain the paths + to templates and static files directories correspondingly + - `debug_env` is a path to `.env_debug` + - `is_debug` is True when DEBUG env variable is set, + and then `.env_debug` is loaded automatically + - `settings` (pydantic.BaseSettings): + - `session_key`, `csrf_key` are secret keys + for WTForms CSRF protection; + generated with secrets.token_urlsafe + if are not set in the environment variables + - `templates` is a Jinja2Templates FastAPI object, + shouldn't be used manually, see + Helper Functions -> respond.with_tmpl + below +#### The database config loaded by `sql/db.py`: + - `sql_settings` (pydantic.BaseSettings): + - `db_host` + - `db_port` + - `db_user` + - `db_password` + - `db_database` + - `db_url` is the MySQL connection URL + generated from the sql_settings configuration; + just edit the line declaring `db_url` in `db.py` + if you are going to use other DBMS, e.g. PostgreSQL + +### Paths +`app/paths` directory contains all FastAPI paths +separated into multiple files. +Each file have a class inside with the `add_paths` method. +This method is called on application startup. + +You can + - add your own paths to the existing files + - rename or delete a file in `app/paths/` + (it's *not* recommended to do this with `errors.py`, + see about [error pages](#custom-error-pages) below) + - create a new file in this directory + copying the contents of `pages.py` + +> **Note** +> In the paths files FastAPI's decorators +are called with `@self.app.`, not just `@app.` + +In case of deleteing/renaming/creating any paths files, +`app/main.py` also must be modified: +1. Find the comment `# Add your own paths...` +2. Add or remove import statements below +3. Add or remove elements in `paths` list below + +### Custom Error Pages +`app/paths/errors.py` automatically adds error handlers +when the application launches. +By default, 404 and 500 HTTP codes are configured +for the custom pages (`templates/404.html` and `500.html`), +but you can add your own: +1. Open `errors.py` +2. Find the comment `# Add other...` +3. Add (or remove) elements to the list below + +### WTForms +TODO + +### Database +TODO + + +## Usage + + + +### Makefile +Make commands: +|Command|Description| +|:-----:|:----------| +|`make format`|Format the code using AutoPEP8| +|`make check`|Check the code with linters (MyPy, Pylint)| +|`make dev`|Run the app in development mode| +|`make prod`|Run a production server| +|`make docker`|Build a docker image from `Dockerfile`| +|`make clean`|Clean all cache| + + +## Helper functions + +### `respond.py` +Import: `from . import respond` + +#### `with_redirect(url=/, code=200, ...)` +Return a redirect to the page specified in `url`. +By default, code is 302 so method is changed to GET. +To leave the same HTTP method, use 307 status code +or call `with_redirect_307` function. +`args` and `kwargs` are passed directly +to the Response contructor. + +**Args:** + url (str, optional): Target URL, root by default + code (int, optional): HTTP response code + +**Returns:** FastAPI's RedirectResponse object + +#### `with_redirect_302(url=/, ...)` +#### `with_redirect_307(url=/, ...)` +As said before, + - HTTP 302 Found redirects to a page + without saving the same method + as in the current request + - HTTP 307 Temporary Redirect + doesn't change the method + (POST request = POST method after redirect) + +#### `with_text(content, code=200, ...)` +Return a plain text to the user. +`args` and `kwargs` are passed directly +to the Response contructor. + +**Args:** + content (str): Plain text content + code (int, optional): HTTP response code + +**Returns:** FastAPI's PlainTextResponse object + +#### `with_template(name, request, code=200, ..., **context)` +#### `with_tmpl(name, request, code=200, ..., **context)` +Render a Jinja2 template and return Response object. +`response_class` parameter is not needed. +A small explanation about the `request` function argument: +```python +from fastapi import Request +from . import respond + +@app.get('/') +async def main_page(req: Request): # <-- + return respond.with_tmpl( + 'index.html', + request=req, # <-- + ... + ) +``` +FastAPI will automatically pass the Request object +to your function if you specify +the correct type hint (`: Request`) + +**Args:** + name (str): Template filename + request (Request): FastAPI request object + code (int, optional): HTTP response code + headers (Optional[Mapping[str, str]], optional): + Additional headers, passed to the Response constructor + background (Optional[BackgroundTask], optional): + Background task, passed to the Response constructor + +**Returns:** FastAPI's TemplateResponse object + +#### `with_file(path, mime=None, code=200, ...)` +Send the file specified in `path` +automatically guessing its mimetype if `mime` is None. +`args` and `kwargs` are passed directly +to the Response contructor. + +**Args:** + path (os.PathLike): File path + mime (Optional[str], optional): File mimetype + code (int, optional): HTTP response code + +**Returns:** FileResponse: FastAPI's FileResponse object + +### `forms/__init__.py` +Import: `from . import forms` + +#### `async get_form(form, req)` +Almost the same as `form.from_formdata`, +and must be used *instead* of instantiating +form object directly as in Flask. +See `respond.with_tmpl` for explanation +about the `request` argument. + +**Args:** + form (Type[StarletteForm]): StarletteForm class + req (Request): Request object + +**Returns:** StarletteForm instance + +### `sql/db.py` + +#### `async get_db()` +FastAPI dependency returning database Session object. +Code is copied from the official docs. + +**Yields:** SQLAlchemy Session object + + +## Publishing app +First of all, build an image: `make docker` +Follow [this documentation page](https://docs.docker.com/get-started/04_sharing_app/) +to upload your image to Docker Hub. diff --git a/app/forms/__init__.py b/app/forms/__init__.py index 9cf5836..e078afd 100644 --- a/app/forms/__init__.py +++ b/app/forms/__init__.py @@ -13,8 +13,10 @@ async def get_form( form: Type[T], req: Request) -> T: """Almost the same as `form.from_formdata`, - and must be used *instead* of instantiatng - form object directly as in Flask + and must be used *instead* of instantiating + form object directly as in Flask. + See `respond.with_tmpl` for explanation + about the `request` argument Args: form (Type[StarletteForm]): StarletteForm class diff --git a/app/respond.py b/app/respond.py index 027d358..51bd00e 100644 --- a/app/respond.py +++ b/app/respond.py @@ -70,16 +70,32 @@ def with_tmpl( background: Optional[BackgroundTask] = None, **context) -> Response: """Render a Jinja2 template and return Response object. - `response_class` parameter is not needed + `response_class` parameter is not needed. + A small explanation about the `request` function argument: + ```python + from fastapi import Request + from . import respond + + @app.get('/') + async def main_page(req: Request): # <-- + return respond.with_tmpl( + 'index.html', + request=req, # <-- + ... + ) + ``` + FastAPI will automatically pass the Request object + to your function if you specify + the correct type hint (`: Request`) Args: name (str): Template filename request (Request): FastAPI request object code (int, optional): HTTP response code headers (Optional[Mapping[str, str]], optional): - Additional headers, passed to Response constructor + Additional headers, passed to the Response constructor background (Optional[BackgroundTask], optional): - Background task, passed to Response constructor + Background task, passed to the Response constructor Returns: FastAPI's TemplateResponse object @@ -102,8 +118,8 @@ def with_file( mime: Optional[str] = None, code: int = 200, *args, **kwargs) -> FileResponse: - """Send a file specified in `path` - automatically guessing mimetype if `mime` is None. + """Send the file specified in `path` + automatically guessing its mimetype if `mime` is None. `args` and `kwargs` are passed directly to the Response contructor @@ -113,7 +129,7 @@ def with_file( code (int, optional): HTTP response code Returns: - FileResponse: FastAPI's FileResponse object + FastAPI's FileResponse object """ return FileResponse(