Compare commits

..

4 commits

25 changed files with 644 additions and 49 deletions

5
.env Normal file
View file

@ -0,0 +1,5 @@
MYSQL_HOST=${REPO_NAME_SNAKE}_db
MUSQL_PORT=3306
MYSQL_USER=${REPO_NAME_SNAKE}
MYSQL_PASSWORD=
MYSQL_DATABASE=${REPO_NAME_SNAKE}

5
.env_debug Normal file
View file

@ -0,0 +1,5 @@
MYSQL_HOST=localhost
MUSQL_PORT=3306
MYSQL_USER=darkcat09
MYSQL_PASSWORD=
MYSQL_DATABASE=apptest

6
.gitea/template Normal file
View file

@ -0,0 +1,6 @@
app/*.py
templates/base.html
docker-compose.yml
Makefile
.env
README.md

54
.gitignore vendored
View file

@ -72,56 +72,23 @@ instance/
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
#.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
# PEP 582
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
@ -129,13 +96,6 @@ ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
@ -153,10 +113,6 @@ dmypy.json
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# IDE
.vscode/
.idea/

7
Dockerfile Normal file
View file

@ -0,0 +1,7 @@
FROM python:3-alpine
RUN apk update && apk upgrade && apk add py-pip make
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD make prod
EXPOSE 8000

20
Makefile Normal file
View file

@ -0,0 +1,20 @@
dev:
DEBUG="true" python3 -m uvicorn main:app --reload
prod:
python3 -m uvicorn main:app
format:
python3 -m autopep8 -r --in-place app/
check:
python3 -m mypy app/
python3 -m pylint app/
docker:
docker build -t ${REPO_OWNER_LOWER}/${REPO_NAME_SNAKE} .
clean:
rm -rf app/__pycache__
rm -rf __pycache__
rm -rf .mypy_cache

0
app/__init__.py Normal file
View file

24
app/common.py Normal file
View file

@ -0,0 +1,24 @@
import secrets
from pathlib import Path
from fastapi.templating import Jinja2Templates
from pydantic import BaseSettings
file_dir = Path(__file__).parent
class Settings(BaseSettings):
secret_key: str = secrets.token_hex(32)
templates_dir: str = str(
file_dir.parent / 'templates'
)
static_dir: str = str(
file_dir.parent / 'static'
)
settings = Settings()
templates = Jinja2Templates(
directory=settings.templates_dir,
)

50
app/errors.py Normal file
View file

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

25
app/main.py Normal file
View file

@ -0,0 +1,25 @@
from typing import List, Type
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from .common import settings
# Add your paths here
from .paths import Paths
from . import pages
from . import errors
paths: List[Type[Paths]] = [
pages.MainPaths,
errors.ErrorsPaths,
]
app = FastAPI()
app.mount(
'/static',
StaticFiles(directory=settings.static_dir),
name='static',
)
for p in paths:
p(app).add_paths()

20
app/pages.py Normal file
View file

@ -0,0 +1,20 @@
"""Main FastAPI paths"""
from fastapi import Request, 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(req: Request) -> Response:
return respond.with_tmpl(
'index.html',
request=req,
)

21
app/paths.py Normal file
View file

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

77
app/respond.py Normal file
View file

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

19
docker-compose.yml Normal file
View file

@ -0,0 +1,19 @@
version: "3"
services:
${REPO_NAME_SNAKE}:
image: ${REPO_OWNER}/${REPO_NAME_SNAKE}:latest
container_name: ${REPO_NAME_SNAKE}
restart: unless-stopped
ports:
- "8080:8000"
links:
- ${REPO_NAME_SNAKE}_db
env_file: .env
${REPO_NAME_SNAKE}_db:
image: mariadb:latest
container_name: ${REPO_NAME_SNAKE}_db
restart: unless-stopped
volumes:
- "./database:/var/lib/mysql"
env_file: .env

9
main.py Executable file
View file

@ -0,0 +1,9 @@
#!/usr/bin/env python3
import uvicorn
from app.main import app
if __name__ == '__main__':
uvicorn.run(app)

187
pylintrc Normal file
View file

@ -0,0 +1,187 @@
[MAIN]
analyse-fallback-blocks=no
extension-pkg-allow-list=
extension-pkg-whitelist=
fail-on=
fail-under=10
ignore=CVS
ignore-paths=
ignore-patterns=^\.#
ignored-modules=
jobs=4
limit-inference-results=100
load-plugins=
persistent=yes
py-version=3.10
recursive=no
suggestion-mode=yes
unsafe-load-any-extension=no
[REPORTS]
evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10))
msg-template=
reports=no
score=yes
[MESSAGES CONTROL]
confidence=HIGH,
CONTROL_FLOW,
INFERENCE,
INFERENCE_FAILURE,
UNDEFINED
disable=raw-checker-failed,
bad-inline-option,
locally-disabled,
file-ignored,
suppressed-message,
useless-suppression,
deprecated-pragma,
use-symbolic-message-instead
enable=c-extension-no-member
[SIMILARITIES]
ignore-comments=yes
ignore-docstrings=yes
ignore-imports=yes
ignore-signatures=yes
min-similarity-lines=4
[MISCELLANEOUS]
notes=FIXME,
XXX,
TODO
notes-rgx=
[DESIGN]
exclude-too-few-public-methods=
ignored-parents=
max-args=5
max-attributes=7
max-bool-expr=5
max-branches=12
max-locals=15
max-parents=7
max-public-methods=20
max-returns=6
max-statements=50
min-public-methods=1
[STRING]
check-quote-consistency=no
check-str-concat-over-line-jumps=no
[CLASSES]
check-protected-access-in-special-methods=no
defining-attr-methods=__init__,
__new__,
setUp,
__post_init__
exclude-protected=_asdict,
_fields,
_replace,
_source,
_make
valid-classmethod-first-arg=cls
valid-metaclass-classmethod-first-arg=cls
[FORMAT]
expected-line-ending-format=
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
indent-after-paren=4
indent-string=' '
max-line-length=100
max-module-lines=1000
single-line-class-stmt=no
single-line-if-stmt=no
[IMPORTS]
allow-any-import-level=
allow-wildcard-with-all=no
deprecated-modules=
ext-import-graph=
import-graph=
int-import-graph=
known-standard-library=
known-third-party=enchant
preferred-modules=
[VARIABLES]
additional-builtins=
allow-global-unused-variables=yes
allowed-redefined-builtins=
callbacks=cb_,
_cb
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
ignored-argument-names=_.*|^ignored_|^unused_
init-import=no
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
[LOGGING]
logging-format-style=old
logging-modules=logging
[EXCEPTIONS]
overgeneral-exceptions=BaseException,
Exception
[BASIC]
argument-naming-style=snake_case
attr-naming-style=snake_case
bad-names=foo,
bar,
baz,
toto,
tutu,
tata
bad-names-rgxs=
class-attribute-naming-style=any
class-const-naming-style=UPPER_CASE
class-naming-style=PascalCase
const-naming-style=any
docstring-min-length=-1
function-naming-style=snake_case
good-names=i,
j,
k,
f,
db,
ex,
Run,
_
good-names-rgxs=
include-naming-hint=no
inlinevar-naming-style=any
method-naming-style=snake_case
module-naming-style=snake_case
name-group=
no-docstring-rgx=^_
property-classes=abc.abstractproperty
variable-naming-style=snake_case
[SPELLING]
max-spelling-suggestions=4
spelling-dict=
spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:
spelling-ignore-words=
spelling-private-dict-file=
spelling-store-unknown-words=no
[TYPECHECK]
contextmanager-decorators=contextlib.contextmanager
generated-members=
ignore-none=yes
ignore-on-opaque-inference=yes
ignored-checks-for-mixins=no-member,
not-async-context-manager,
not-context-manager,
attribute-defined-outside-init
ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace
missing-member-hint=yes
missing-member-hint-distance=1
missing-member-max-choices=1
mixin-class-rgx=.*[Mm]ixin
signature-mutators=
[REFACTORING]
max-nested-blocks=5
never-returning-functions=sys.exit,argparse.parse_error

5
requirements.txt Normal file
View file

@ -0,0 +1,5 @@
fastapi==0.92.0
uvicorn[standard]==0.20.0
jinja2==3.1.2
starlette-wtf==0.4.3
python-dotenv==0.21.1

60
static/css/style.css Normal file
View file

@ -0,0 +1,60 @@
body {
height: 100vh;
padding: 0;
margin: 0;
font-family: sans-serif;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
--bg: #fff;
--fg: #000;
background: var(--bg);
color: var(--fg);
}
@media (prefers-color-scheme: dark) {
body {
--bg: #202023;
--fg: #eee;
}
}
header { margin-top: 5px; }
footer { margin-bottom: 5px; }
a {
color: #5b8a55;
}
a:hover {
filter: brightness(120%);
}
form {
display: flex;
flex-direction: column;
}
form > div {
margin-top: 5px;
display: flex;
flex-direction: row;
justify-content: end;
}
form > div > label {
width: 50%;
margin-right: 10px;
}
form > div > input {
width: 50%;
border: 1px solid var(--fg);
outline: none;
background: var(--bg);
color: var(--fg);
}
form > div > input:hover,
form > div > input:focus {
filter: brightness(130%);
}

4
static/js/script.js Normal file
View file

@ -0,0 +1,4 @@
addEventListener('load', () => {
document.getElementById('js')
.innerText = new Date().toLocaleString()
})

11
templates/404.html Normal file
View file

@ -0,0 +1,11 @@
{% extends "base.html" %}
{% block title %}404{% endblock %}
{% block content %}
<h1>404: Not Found</h1>
<p>
Go to the
<a href="/">main page</a>
</p>
{% endblock %}

15
templates/500.html Normal file
View file

@ -0,0 +1,15 @@
{% extends "base.html" %}
{% block title %}500{% endblock %}
{% block content %}
<h1>500: ISE</h1>
<p>
An error occured while
processing your request.
</p>
<p>
Please, try again later
or contact web site admin.
</p>
{% endblock %}

21
templates/admin.html Normal file
View file

@ -0,0 +1,21 @@
{% extends "base.html" %}
{% block title %}Admin{% endblock %}
{% block content %}
<h1>Add a person to DB</h1>
<form action="/add" method="post">
{{ form.hidden_tag() }}
{% for field in form %}
{% if field.name != 'csrf_token' %}
<div>
{{ field.label }}
{{ field }}
</div>
{% endif %}
{% endfor %}
<div>
<input type="submit" value="Add">
</div>
</form>
{% endblock %}

18
templates/base.html Normal file
View file

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="${REPO_DESCRIPTION}">
<title>{% block title %}{% endblock %} | ${REPO_NAME}</title>
<link rel="stylesheet" href="/static/css/style.css">
<script src="/static/js/script.js"></script>
</head>
<body>
<header>${REPO_NAME}</header>
<article>
{% block content %}{% endblock %}
</article>
<footer id="js"></footer>
</body>
</html>

12
templates/index.html Normal file
View file

@ -0,0 +1,12 @@
{% extends "base.html" %}
{% block title %}Main page{% endblock %}
{% block content %}
<h1>
This is the default main page of
<a href="https://git.dc09.ru/DarkCat09/tmpl-fastapi">
FastAPI app template
</a>
</h1>
{% endblock %}

18
templates/table.html Normal file
View file

@ -0,0 +1,18 @@
{% extends "base.html" %}
{% block title %}Database{% endblock %}
{% block content %}
<h1>Sample database</h1>
<table>
<tbody>
{% for row in rows %}
<tr>
{% for cell in row %}
<td>{{ cell }}</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}