Compare commits
No commits in common. "3c6da6a49a63c6dbd91ede8057ed1aa2526efaf4" and "6b9d616808ed0b7b35db915a8dfcee836ebe2e9d" have entirely different histories.
3c6da6a49a
...
6b9d616808
5 changed files with 11 additions and 267 deletions
|
@ -1,5 +1,3 @@
|
||||||
SESSION_KEY=debug
|
|
||||||
CSRF_KEY=debug
|
|
||||||
DB_HOST=localhost
|
DB_HOST=localhost
|
||||||
DB_PORT=3306
|
DB_PORT=3306
|
||||||
DB_USER=darkcat09
|
DB_USER=darkcat09
|
||||||
|
|
239
README.md
239
README.md
|
@ -1,238 +1,3 @@
|
||||||
# FastAPI template
|
# tmpl-fastapi
|
||||||
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.
|
|
|
@ -13,10 +13,8 @@ async def get_form(
|
||||||
form: Type[T],
|
form: Type[T],
|
||||||
req: Request) -> T:
|
req: Request) -> T:
|
||||||
"""Almost the same as `form.from_formdata`,
|
"""Almost the same as `form.from_formdata`,
|
||||||
and must be used *instead* of instantiating
|
and must be used *instead* of instantiatng
|
||||||
form object directly as in Flask.
|
form object directly as in Flask
|
||||||
See `respond.with_tmpl` for explanation
|
|
||||||
about the `request` argument
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
form (Type[StarletteForm]): StarletteForm class
|
form (Type[StarletteForm]): StarletteForm class
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
"""Custom error pages for FastAPI app"""
|
"""Custom error pages for FastAPI app"""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List
|
|
||||||
|
|
||||||
from fastapi import Request, Response
|
from fastapi import Request, Response
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
|
@ -11,7 +10,7 @@ from .. import respond
|
||||||
from .. import common
|
from .. import common
|
||||||
|
|
||||||
# Add other HTTP error codes
|
# Add other HTTP error codes
|
||||||
codes: List[int] = [404, 500]
|
codes = [404, 500]
|
||||||
|
|
||||||
|
|
||||||
class ErrorsPaths(Paths):
|
class ErrorsPaths(Paths):
|
||||||
|
|
|
@ -70,32 +70,16 @@ def with_tmpl(
|
||||||
background: Optional[BackgroundTask] = None,
|
background: Optional[BackgroundTask] = None,
|
||||||
**context) -> Response:
|
**context) -> Response:
|
||||||
"""Render a Jinja2 template and return Response object.
|
"""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:
|
Args:
|
||||||
name (str): Template filename
|
name (str): Template filename
|
||||||
request (Request): FastAPI request object
|
request (Request): FastAPI request object
|
||||||
code (int, optional): HTTP response code
|
code (int, optional): HTTP response code
|
||||||
headers (Optional[Mapping[str, str]], optional):
|
headers (Optional[Mapping[str, str]], optional):
|
||||||
Additional headers, passed to the Response constructor
|
Additional headers, passed to Response constructor
|
||||||
background (Optional[BackgroundTask], optional):
|
background (Optional[BackgroundTask], optional):
|
||||||
Background task, passed to the Response constructor
|
Background task, passed to Response constructor
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
FastAPI's TemplateResponse object
|
FastAPI's TemplateResponse object
|
||||||
|
@ -118,8 +102,8 @@ def with_file(
|
||||||
mime: Optional[str] = None,
|
mime: Optional[str] = None,
|
||||||
code: int = 200,
|
code: int = 200,
|
||||||
*args, **kwargs) -> FileResponse:
|
*args, **kwargs) -> FileResponse:
|
||||||
"""Send the file specified in `path`
|
"""Send a file specified in `path`
|
||||||
automatically guessing its mimetype if `mime` is None.
|
automatically guessing mimetype if `mime` is None.
|
||||||
`args` and `kwargs` are passed directly
|
`args` and `kwargs` are passed directly
|
||||||
to the Response contructor
|
to the Response contructor
|
||||||
|
|
||||||
|
@ -129,7 +113,7 @@ def with_file(
|
||||||
code (int, optional): HTTP response code
|
code (int, optional): HTTP response code
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
FastAPI's FileResponse object
|
FileResponse: FastAPI's FileResponse object
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return FileResponse(
|
return FileResponse(
|
||||||
|
|
Loading…
Add table
Reference in a new issue