1
0
mirror of https://github.com/ijaric/voice_assistant.git synced 2025-05-24 14:33:26 +00:00

add fastapi template

This commit is contained in:
grucshetskyaleksei 2023-09-19 01:15:40 +03:00
parent b606a21951
commit bebbcb9b5c
32 changed files with 1976 additions and 2 deletions

View File

@ -0,0 +1,2 @@
.venv
.env

10
src/fastapi/.env.example Normal file
View File

@ -0,0 +1,10 @@
db_host=db
db_port=5432
db_user=user
db_password=Qwe123
db_name=notification_admin
server_host=0.0.0.0
server_port=8000
jwt_secret_key=v9LctjUWwol4XbvczPiLFMDtZ8aal7mm

15
src/fastapi/Dockerfile Normal file
View File

@ -0,0 +1,15 @@
FROM python:3.11
WORKDIR /opt/app
ENV PYTHONPATH '/opt/app'
COPY pyproject.toml ./
RUN pip install poetry \
&& poetry config virtualenvs.create false \
&& poetry install --no-dev
COPY backend/bin bin
COPY backend/libs libs
COPY entrypoint.sh .
RUN chmod +x /opt/app/entrypoint.sh

View File

View File

@ -0,0 +1,16 @@
import logging
import uvicorn
import libs.app.app as app_module
from libs.app import settings as libs_app_settings
logger = logging.getLogger(__name__)
app_instance = app_module.Application()
app = app_instance.create_app()
settings = libs_app_settings.get_settings()
if __name__ == "__main__":
uvicorn.run(app, host=settings.api.host, port=settings.api.port)

View File

View File

View File

@ -0,0 +1,27 @@
import uuid
import sqlalchemy
from sqlalchemy import Column, DateTime
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.ext.declarative import declared_attr
class BaseMixin:
@declared_attr
def id(cls):
return Column(
UUID(as_uuid=True),
primary_key=True,
default=uuid.uuid4,
unique=True,
nullable=False,
)
@declared_attr
def created_at(cls):
return Column(DateTime, server_default=sqlalchemy.func.now())
@declared_attr
def updated_at(cls):
return Column(DateTime, server_default=sqlalchemy.func.now())

View File

@ -0,0 +1,8 @@
import uuid
import pydantic
class Token(pydantic.BaseModel):
sub: uuid.UUID
exp: int | None = None

View File

@ -0,0 +1,27 @@
from fastapi import FastAPI, HTTPException, Security, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from jose import JWTError, jwt
from libs.api.schemas.entity import Token
from libs.app import settings as libs_app_settings
from pydantic import ValidationError
app = FastAPI()
settings = libs_app_settings.get_settings()
security = HTTPBearer()
def get_token_data(
authorization: HTTPAuthorizationCredentials = Security(security),
) -> Token:
token = authorization.credentials
try:
secret_key = settings.jwt_secret_key
payload = jwt.decode(token, secret_key, algorithms=["HS256"])
return Token(**payload)
except (JWTError, ValidationError):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
)

View File

View File

@ -0,0 +1,41 @@
import logging
import logging.config as logging_config
import fastapi
import libs.api.handlers as admin_api_handlers
from .logger import LOGGING
from .settings import get_settings
logging_config.dictConfig(LOGGING)
logger = logging.getLogger(__name__)
class Application:
def __init__(self) -> None:
self.settings = get_settings()
self.logger = logging.getLogger(__name__)
self.producer = None
def create_app(self) -> fastapi.FastAPI:
app = fastapi.FastAPI(
title="FastAPI",
version="0.1.0",
docs_url="/api/openapi",
openapi_url="/api/openapi.json",
default_response_class=fastapi.responses.ORJSONResponse,
)
# app.include_router(admin_api_handlers.user_router, prefix="/api/v1/users", tags=["users"])
# app.include_router(admin_api_handlers.movie_router, prefix="/api/v1/movies", tags=["movies"])
@app.on_event("startup")
async def startup_event():
self.logger.info("Starting server")
@app.on_event("shutdown")
async def shutdown_event():
self.logger.info("Shutting down server")
return app

View File

@ -0,0 +1,70 @@
import pydantic
import pydantic_settings
class LoggingSettings(pydantic_settings.BaseSettings):
log_format: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
log_default_handlers: list[str] = [
"console",
]
log_level_handlers: str = "DEBUG"
log_level_loggers: str = "INFO"
log_level_root: str = "INFO"
log_settings = LoggingSettings()
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"verbose": {"format": log_settings.log_format},
"default": {
"()": "uvicorn.logging.DefaultFormatter",
"fmt": "%(levelprefix)s %(message)s",
"use_colors": None,
},
"access": {
"()": "uvicorn.logging.AccessFormatter",
"fmt": "%(levelprefix)s %(client_addr)s - '%(request_line)s' %(status_code)s",
},
},
"handlers": {
"console": {
"level": log_settings.log_level_handlers,
"class": "logging.StreamHandler",
"formatter": "verbose",
},
"default": {
"formatter": "default",
"class": "logging.StreamHandler",
"stream": "ext://sys.stdout",
},
"access": {
"formatter": "access",
"class": "logging.StreamHandler",
"stream": "ext://sys.stdout",
},
},
"loggers": {
"": {
"handlers": log_settings.log_default_handlers,
"level": log_settings.log_level_loggers,
},
"uvicorn.error": {
"level": log_settings.log_level_loggers,
},
"uvicorn.access": {
"handlers": ["access"],
"level": log_settings.log_level_loggers,
"propagate": False,
},
},
"root": {
"level": log_settings.log_level_root,
"formatter": "verbose",
"handlers": log_settings.log_default_handlers,
},
}

View File

@ -0,0 +1,36 @@
import functools
import pydantic_settings
from dotenv import load_dotenv
from pydantic import Field
load_dotenv('.env.dev')
class DbSettings(pydantic_settings.BaseSettings):
model_config = pydantic_settings.SettingsConfigDict(env_prefix="db_")
host: str = "localhost"
port: int = 5432
user: str
password: str
name: str
class ApiSettings(pydantic_settings.BaseSettings):
model_config = pydantic_settings.SettingsConfigDict(env_prefix="server_")
host: str = "0.0.0.0"
port: int = 8000
class Settings(pydantic_settings.BaseSettings):
db: DbSettings = Field(default_factory=DbSettings)
api: ApiSettings = Field(default_factory=ApiSettings)
jwt_secret_key: str
@functools.lru_cache
def get_settings() -> Settings:
return Settings()

View File

View File

@ -0,0 +1,3 @@
# from libs.api.models.movies import *
# from libs.api.models.notification_templates import *
# from libs.api.models.users import *

View File

@ -0,0 +1,31 @@
import typing
from libs.app import settings as libs_app_settings
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.orm import DeclarativeBase
settings = libs_app_settings.get_settings()
# Создаём базовый класс для будущих моделей
class Base(DeclarativeBase):
pass
# Создаём движок
# Настройки подключения к БД передаём из переменных окружения, которые заранее загружены в файл настроек
database_dsn = (
f"postgresql+asyncpg://{settings.db.user}:{settings.db.password}"
f"@{settings.db.host}:{settings.db.port}/{settings.db.name}"
)
engine = create_async_engine(database_dsn, echo=True, future=True)
async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async def get_session() -> typing.AsyncGenerator[AsyncSession, typing.Any]:
async with async_session() as session:
yield session

View File

@ -0,0 +1,64 @@
version: '3'
services:
db:
image: postgres:15.2
environment:
POSTGRES_USER: ${db_user}
POSTGRES_PASSWORD: ${db_password}
POSTGRES_DB: ${db_name}
env_file:
- .env
ports:
- "${db_port}:5432"
volumes:
- postgres_data:/var/lib/postgresql/data/
restart: always
networks:
- backend_network
api:
build:
context: .
container_name: fastapi
image: fastapi_app
restart: always
entrypoint: ["/opt/app/entrypoint.sh"]
ports:
- "${server_port}:5432"
depends_on:
- db
env_file:
- .env
networks:
- backend_network
- api_network
nginx:
image: nginx:1.23.4
volumes:
- ./nginx:/etc/nginx/:ro
depends_on:
- api
ports:
- "80:80"
networks:
- api_network
redis:
image: redis:7.0.11
restart: always
command: redis-server --bind 0.0.0.0
ports:
- "6379:6379"
networks:
- backend_network
volumes:
postgres_data:
networks:
api_network:
driver: bridge
backend_network:
driver: bridge

View File

@ -0,0 +1,60 @@
version: '3'
services:
db:
image: postgres:15.2
environment:
POSTGRES_USER: ${db_user}
POSTGRES_PASSWORD: ${db_password}
POSTGRES_DB: ${db_name}
env_file:
- .env
ports:
- "127.0.0.1:${db_port}:5432"
volumes:
- postgres_data:/var/lib/postgresql/data/
restart: always
networks:
- backend_network
api:
build:
context: .
container_name: fastapi
image: fastapi_app
restart: always
entrypoint: ["/opt/app/entrypoint.sh"]
depends_on:
- db
env_file:
- .env
networks:
- backend_network
- api_network
nginx:
image: nginx:1.23.4
volumes:
- ./nginx:/etc/nginx/:ro
depends_on:
- api
ports:
- "80:80"
networks:
- api_network
redis:
image: redis:7.0.11
restart: always
command: redis-server --bind 0.0.0.0
networks:
- backend_network
volumes:
postgres_data:
networks:
api_network:
driver: bridge
backend_network:
driver: bridge

View File

@ -0,0 +1,7 @@
#!/bin/bash
while ! (echo > /dev/tcp/db/5432) >/dev/null 2>&1; do
sleep 1
done
exec python -m bin

View File

@ -0,0 +1,10 @@
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
location /api {
proxy_pass http://api:8000/api;
}
}

View File

@ -0,0 +1,98 @@
types {
text/html html htm shtml;
text/css css;
text/xml xml;
image/gif gif;
image/jpeg jpeg jpg;
application/javascript js;
application/atom+xml atom;
application/rss+xml rss;
text/mathml mml;
text/plain txt;
text/vnd.sun.j2me.app-descriptor jad;
text/vnd.wap.wml wml;
text/x-component htc;
image/avif avif;
image/png png;
image/svg+xml svg svgz;
image/tiff tif tiff;
image/vnd.wap.wbmp wbmp;
image/webp webp;
image/x-icon ico;
image/x-jng jng;
image/x-ms-bmp bmp;
font/woff woff;
font/woff2 woff2;
application/java-archive jar war ear;
application/json json;
application/mac-binhex40 hqx;
application/msword doc;
application/pdf pdf;
application/postscript ps eps ai;
application/rtf rtf;
application/vnd.apple.mpegurl m3u8;
application/vnd.google-earth.kml+xml kml;
application/vnd.google-earth.kmz kmz;
application/vnd.ms-excel xls;
application/vnd.ms-fontobject eot;
application/vnd.ms-powerpoint ppt;
application/vnd.oasis.opendocument.graphics odg;
application/vnd.oasis.opendocument.presentation odp;
application/vnd.oasis.opendocument.spreadsheet ods;
application/vnd.oasis.opendocument.text odt;
application/vnd.openxmlformats-officedocument.presentationml.presentation
pptx;
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
xlsx;
application/vnd.openxmlformats-officedocument.wordprocessingml.document
docx;
application/vnd.wap.wmlc wmlc;
application/wasm wasm;
application/x-7z-compressed 7z;
application/x-cocoa cco;
application/x-java-archive-diff jardiff;
application/x-java-jnlp-file jnlp;
application/x-makeself run;
application/x-perl pl pm;
application/x-pilot prc pdb;
application/x-rar-compressed rar;
application/x-redhat-package-manager rpm;
application/x-sea sea;
application/x-shockwave-flash swf;
application/x-stuffit sit;
application/x-tcl tcl tk;
application/x-x509-ca-cert der pem crt;
application/x-xpinstall xpi;
application/xhtml+xml xhtml;
application/xspf+xml xspf;
application/zip zip;
application/octet-stream bin exe dll;
application/octet-stream deb;
application/octet-stream dmg;
application/octet-stream iso img;
application/octet-stream msi msp msm;
audio/midi mid midi kar;
audio/mpeg mp3;
audio/ogg ogg;
audio/x-m4a m4a;
audio/x-realaudio ra;
video/3gpp 3gpp 3gp;
video/mp2t ts;
video/mp4 mp4;
video/mpeg mpeg mpg;
video/quicktime mov;
video/webm webm;
video/x-flv flv;
video/x-m4v m4v;
video/x-mng mng;
video/x-ms-asf asx asf;
video/x-ms-wmv wmv;
video/x-msvideo avi;
}

View File

@ -0,0 +1,37 @@
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
server_tokens off;
sendfile on;
tcp_nodelay on;
tcp_nopush on;
client_max_body_size 200m;
gzip on;
gzip_comp_level 3;
gzip_min_length 1000;
gzip_types
text/plain
text/css
application/json
application/x-javascript
text/xml
text/javascript;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
include conf.d/site.conf;
}

1265
src/fastapi/poetry.lock generated Normal file

File diff suppressed because it is too large Load Diff

147
src/fastapi/pyproject.toml Normal file
View File

@ -0,0 +1,147 @@
[build-system]
build-backend = "poetry.core.masonry.api"
requires = ["poetry-core"]
[tool.black]
line-length = 120
target-version = ['py311']
[tool.isort]
known_first_party = ["backend", "tests"]
line_length = 120
profile = "black"
py_version = "311"
[tool.poetry]
authors = ["jsdio@jsdio.ru"]
description = ""
name = "fastapi_project"
readme = "README.md"
version = "0.1.0"
[tool.poetry.dependencies]
python = "^3.11"
fastapi = "0.103.1"
sqlalchemy = "^2.0.20"
alembic = "^1.12.0"
asyncpg = "^0.28.0"
psycopg2 = "^2.9.7"
python-jose = "^3.3.0"
uvicorn = "^0.23.2"
pydantic = {extras = ["email"], version = "^2.3.0"}
pydantic-settings = "^2.0.3"
[tool.poetry.dev-dependencies]
black = "^23.7.0"
isort = "^5.12.0"
pylint = "^2.17.5"
pylint-pydantic = "^0.2.4"
pylint-pytest = "^1.1.2"
pyright = "^1.1.318"
pyupgrade = "^3.10.1"
ruff = "^0.0.282"
sort-all = "^1.2.0"
toml-sort = "^0.23.1"
[tool.pylint]
disable = [
"broad-except",
"cannot-enumerate-pytest-fixtures",
"consider-using-from-import",
"consider-using-sys-exit",
"duplicate-code",
"fixme",
"missing-docstring",
"no-member",
"protected-access",
"too-few-public-methods",
"too-many-instance-attributes",
"too-many-locals",
"too-many-statements",
"unnecessary-ellipsis"
]
extension-pkg-allow-list = [
"orjson",
"pydantic"
]
ignore-path = [
"^.*venv/.*$"
]
load-plugins = [
"pylint_pydantic",
"pylint_pytest"
]
max-args = 15
max-line-length = 120
recursive = true
[tool.pylint.basic]
argument-rgx = "^_{0,2}[a-z][a-z0-9]*$"
attr-rgx = "^_{0,2}[a-z][a-z0-9]*$"
class-attribute-rgx = "^_{0,2}[a-zA-Z][a-zA-Z0-9_]*$"
variable-rgx = "^_{0,2}[a-z][a-z0-9_]*$"
[tool.pyright]
exclude = [
".venv"
]
pythonPlatform = "All"
pythonVersion = "3.11"
reportConstantRedefenition = "none"
reportMissingTypeStubs = "none"
reportPrivateUsage = "information"
reportPropertyTypeMismatch = "warning"
reportUninitializedInstanceVariable = "warning"
reportUnknownMemberType = "none"
reportUnnecessaryTypeIgnoreComment = "warning"
reportUntypedFunctionDecorator = "warning"
typeCheckingMode = "strict"
useLibraryCodeForTypes = true
venv = ".venv"
venvPath = "."
[tool.ruff]
ignore = [
# Pyright automatically infers the type of `self`
"ANN101",
# Pyright automatically infers the type of `cls`
"ANN102",
# In some cases actively detrimental; somewhat conflicts with black
"COM",
# Ignore missing docstrings
"D102",
# In combination with D213, this results in noisy diffs and inconsistencies
# See also <https://github.com/charliermarsh/ruff/issues/4174>.
"D200",
# This results inconsistencies between function and class docstrings
# See also <https://github.com/charliermarsh/ruff/issues/4175>.
"D202",
# D211 is preferred since the extra blank line isn't visually useful
"D203",
# D213 is preferred since it's more readable and allows more characters
"D212",
# Ignore missing docstrings
"D414",
# Covered by D401, which is more restrictive
"D415",
# Type-checkers interpret redundant `as` as exporting an item
"PLC0414",
# Causes churn and awful looking import blocks for little gain
"TCH"
]
select = ["ALL"]
[tool.ruff.per-file-ignores]
"tests/*" = [
"D100",
"D103",
"D104",
"S101"
]
[tool.tomlsort]
all = true
ignore_case = true
in_place = true

View File

View File

@ -28,7 +28,7 @@ class App:
del self._faker_client
if __name__ == "__main__":
if __name__ == "__main__.py":
app = App()
try:
app.run()

View File

@ -33,5 +33,5 @@ def main() -> None:
exit(os.EX_SOFTWARE)
if __name__ == "__main__":
if __name__ == "__main__.py":
main()