Compare commits

...

2 commits

5 changed files with 267 additions and 11 deletions

View file

@ -1,3 +1,5 @@
SESSION_KEY=debug
CSRF_KEY=debug
DB_HOST=localhost
DB_PORT=3306
DB_USER=darkcat09

239
README.md
View file

@ -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
## 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
<!--
1. Create a repository from this template
2. For debugging, open `.env_debug` file and
set host, user, password corresponding to
the configuration of MySQL/MariaDB on your PC
3. Edit `paths/pages.py`: add your own paths
in the `add_paths` method
4. Create your own Jinja templates in `templates/` directory,
I recommend to edit `base.html` and inherit other templates from it
5. Edit and rename `forms/users.py`, it contains WTForms classes
6. Customize error pages and add your own paths ...
7. Edit `db/schema.sql` corresponding to your database structure
8. Check `Makefile`, `Dockerfile`, `docker-compose.yml`
9. Run formatter and linters (`make format`, then `make check`)
-->
### 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.

View file

@ -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

View file

@ -1,6 +1,7 @@
"""Custom error pages for FastAPI app"""
from pathlib import Path
from typing import List
from fastapi import Request, Response
from fastapi import HTTPException
@ -10,7 +11,7 @@ from .. import respond
from .. import common
# Add other HTTP error codes
codes = [404, 500]
codes: List[int] = [404, 500]
class ErrorsPaths(Paths):

View file

@ -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(