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

Merge remote-tracking branch 'refs/remotes/origin/main'

This commit is contained in:
Artem Litvinov 2023-09-19 00:05:45 +01:00
commit 66691e0bde
38 changed files with 1801 additions and 2 deletions

View File

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

View File

@ -0,0 +1,11 @@
LOG_LEVEL_HANDLERS=INFO
LOG_LEVEL_LOGGERS=INFO
LOG_LEVEL_ROOT=INFO
API_PORT=8000
NGINX_PORT=80
PROJET_NAME="FastAPI Template"
PROJECT_DESCRIPTION="FastAPI Template Project using DDD"
PROJECT_VERSION=0.0.1

View File

@ -0,0 +1,18 @@
FROM python:3.11
RUN apt-get update
WORKDIR /opt/app
ENV PYTHONPATH '/opt/app'
COPY pyproject.toml ./
COPY poetry.lock ./
RUN apt-get update \
&& pip install poetry \
&& poetry config virtualenvs.create false \
&& poetry install --no-dev
COPY backend .
# CMD ["python", "-m", "bin"]

View File

@ -0,0 +1,29 @@
version: "3"
services:
api:
build:
context: .
ports:
- "${API_PORT}:${API_PORT}"
expose:
- ${API_PORT}
env_file:
- .env
nginx:
image: nginx:1.25.1
restart: always
env_file:
- .env
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/templates:/etc/nginx/templates
ports:
- "${NGINX_PORT}:${NGINX_PORT}"
depends_on:
- api
networks:
default:
driver: bridge

View File

@ -0,0 +1,38 @@
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/api.conf;
include conf.d/rabbitmq.conf;
}

View File

@ -0,0 +1,11 @@
server {
listen ${NGINX_PORT} default_server;
listen [::]:${NGINX_PORT} default_server;
server_name _;
location /api {
proxy_pass http://api:${API_PORT}/api;
proxy_set_header X-Request-Id $request_id;
}
}

View File

@ -14,8 +14,8 @@ profile = "black"
[tool.poetry]
authors = ["name <name@email.com>"]
description = "FastAPI Template Project using DDD"
name = "FastAPI Exmaple"
version = "0.1.0"
name = "FastAPI Template"
version = "0.0.1"
[tool.poetry.dependencies]
black = "^23.9.1"

View File

@ -0,0 +1,2 @@
.venv/
tests/

13
src/python-service/.gitignore vendored Normal file
View File

@ -0,0 +1,13 @@
# Python dependencies
.venv/
# Python cache
__pycache__/
.pytest_cache/
# Coverage reports
/.coverage
htmlcov/
# Environment variables
.env

View File

@ -0,0 +1,11 @@
{
"*.py": [
".venv/bin/python -m black --check",
".venv/bin/python -m isort --check",
".venv/bin/python -m sort_all",
".venv/bin/python -m pyupgrade --py310-plus",
".venv/bin/python -m pylint",
"../node_modules/.bin/pyright"
],
"*.toml": [".venv/bin/toml-sort --check"]
}

View File

@ -0,0 +1,12 @@
# Python dependencies
.venv/
# Python cache
__pycache__/
.pytest_cache/
# Coverage reports
/.coverage
# Environment variables
.env

View File

@ -0,0 +1,19 @@
FROM ghcr.io/yp-middle-python-24/python-dev:3.10-jammy-0.0.1 AS builder
RUN mkdir --parents /opt/app
COPY pyproject.toml /opt/app/pyproject.toml
COPY poetry.lock /opt/app/poetry.lock
COPY poetry.toml /opt/app/poetry.toml
WORKDIR /opt/app
RUN poetry install --no-dev
FROM ghcr.io/yp-middle-python-24/python:3.10-jammy-0.0.1 AS runtime
RUN mkdir --parents /opt/app
COPY --from=builder /opt/app/.venv /opt/app/.venv
COPY bin /opt/app/bin
COPY lib /opt/app/lib
WORKDIR /opt/app
CMD [".venv/bin/python", "-m", "bin.main"]

View File

@ -0,0 +1,3 @@
include ../../common_makefile.mk
PROJECT_FOLDERS = bin lib tests

View File

@ -0,0 +1,28 @@
# Python Service Example Backend
Python Service Example Backend
## Development
### Global dependencies
- poetry
### Makefile commands
- `make init` - Initialize service
- `make lint` - Lint service
- `make lint-fix` - Auto-fix service
- `make test` - Test service
- `make clean` - Clean up service
- `make dev-server-start` - Start dev server
- `make test-coverage-run` - Collect test coverage data
- `make test-coverage-report` - Show coverage report in console
- `make test-coverage-html` - Prepare and show coverage report in browser
- `make ci-image-build` - Build production container
- `make ci-image-push` - Push production container to yccr
### Environment variables
- `APP_ENV` - Application environment (development, production, etc.)
- `APP_VERSION` - Application version

View File

View File

View File

@ -0,0 +1,37 @@
import asyncio
import logging
import os
import lib.app as app
logger = logging.getLogger(__name__)
async def run() -> None:
settings = app.Settings()
application = app.Application.from_settings(settings)
try:
await application.start()
finally:
await application.dispose()
def main() -> None:
try:
asyncio.run(run())
exit(os.EX_OK)
except SystemExit:
exit(os.EX_OK)
except app.ApplicationError:
exit(os.EX_SOFTWARE)
except KeyboardInterrupt:
logger.info("Exited with keyboard interruption")
exit(os.EX_OK)
except BaseException:
logger.exception("Unexpected error occurred")
exit(os.EX_SOFTWARE)
if __name__ == "__main__":
main()

View File

View File

View File

@ -0,0 +1,5 @@
from .liveness_probe import LivenessProbeHandler
__all__ = [
"LivenessProbeHandler",
]

View File

@ -0,0 +1,18 @@
import json
import aiohttp.web as aiohttp_web
import lib.utils.aiohttp as aiohttp_utils
class LivenessProbeHandler(aiohttp_utils.HandlerProtocol):
async def process(self, request: aiohttp_web.Request) -> aiohttp_web.Response:
return aiohttp_web.Response(
status=200,
body=json.dumps(obj={"status": "healthy"}),
)
__all__ = [
"LivenessProbeHandler",
]

View File

@ -0,0 +1,11 @@
from .app import Application
from .errors import ApplicationError, DisposeError, StartServerError
from .settings import Settings
__all__ = [
"Application",
"ApplicationError",
"DisposeError",
"Settings",
"StartServerError",
]

View File

@ -0,0 +1,131 @@
import asyncio
import dataclasses
import logging
import typing
import aiohttp.web as aiohttp_web
import typing_extensions
import lib.api.rest.v1.health as health_handlers
import lib.app.errors as app_errors
import lib.app.settings as app_settings
logger = logging.getLogger(__name__)
@dataclasses.dataclass
class DisposableResource:
name: str
dispose_callback: typing.Awaitable[typing.Any]
class Application:
def __init__(
self,
settings: app_settings.Settings,
aio_app: aiohttp_web.Application,
disposable_resources: typing.Sequence[DisposableResource],
) -> None:
self._settings = settings
self._aio_app = aio_app
self._disposable_resources = disposable_resources
@classmethod
def from_settings(cls, settings: app_settings.Settings) -> typing_extensions.Self:
# Logging
logging.basicConfig(
level=settings.LOGS_MIN_LEVEL,
format=settings.LOGS_FORMAT,
)
logger.info("Initializing application")
disposable_resources = []
# Global clients
logger.info("Initializing global clients")
# Clients
logger.info("Initializing clients")
# Repositories
logger.info("Initializing repositories")
# Caches
logger.info("Initializing caches")
# Services
logger.info("Initializing services")
# Handlers
logger.info("Initializing handlers")
liveness_probe_handler = health_handlers.LivenessProbeHandler()
logger.info("Creating application")
aio_app = aiohttp_web.Application()
# Routes
aio_app.add_routes(
[
aiohttp_web.get(
"/api/v1/health/liveness",
liveness_probe_handler.process,
),
]
)
application = Application(
settings=settings,
aio_app=aio_app,
disposable_resources=disposable_resources,
)
logger.info("Initializing application finished")
return application
async def start(self) -> None:
logger.info("Discord server is starting")
try:
await aiohttp_web._run_app(
app=self._aio_app,
host=self._settings.SERVER_HOST,
port=self._settings.SERVER_PORT,
)
except asyncio.CancelledError:
logger.info("HTTP server has been interrupted")
except BaseException as unexpected_error:
logger.exception("HTTP server failed to start")
raise app_errors.StartServerError("HTTP server failed to start") from unexpected_error
async def dispose(self) -> None:
logger.info("Application is shutting down...")
dispose_errors = []
for resource in self._disposable_resources:
logger.info("Disposing %s...", resource.name)
try:
await resource.dispose_callback
except Exception as unexpected_error:
dispose_errors.append(unexpected_error)
logger.exception("Failed to dispose %s", resource.name)
else:
logger.info("%s has been disposed", resource.name)
if len(dispose_errors) != 0:
logger.error("Application has shut down with errors")
raise app_errors.DisposeError("Application has shut down with errors, see logs above")
logger.info("Application has successfully shut down")
__all__ = [
"Application",
]

View File

@ -0,0 +1,22 @@
import typing
class ApplicationError(Exception):
def __init__(self, message: str, *args: typing.Any) -> None:
super().__init__(*args)
self.message = message
class DisposeError(ApplicationError):
pass
class StartServerError(ApplicationError):
pass
__all__ = [
"ApplicationError",
"DisposeError",
"StartServerError",
]

View File

@ -0,0 +1,32 @@
import typing
import pydantic
LogLevel = typing.Literal["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"]
class Settings(pydantic.BaseSettings):
# App
APP_ENV: str = "development"
APP_NAME: str = "discord-chatbot-backend"
APP_VERSION: str = "0.0.1"
# Logging
LOGS_MIN_LEVEL: LogLevel = "DEBUG"
LOGS_FORMAT: str = "%(asctime)s | %(name)s | %(levelname)s | %(message)s"
# Server
SERVER_HOST: str = "localhost"
SERVER_PORT: int = 8080
@property
def is_development(self) -> bool:
return self.APP_ENV == "development"
__all__ = [
"Settings",
]

View File

View File

@ -0,0 +1,5 @@
from .types import HandlerProtocol
__all__ = [
"HandlerProtocol",
]

View File

@ -0,0 +1,5 @@
from .handler import HandlerProtocol
__all__ = [
"HandlerProtocol",
]

View File

@ -0,0 +1,13 @@
import typing
import aiohttp.web as aiohttp_web
class HandlerProtocol(typing.Protocol):
async def process(self, request: aiohttp_web.Request) -> aiohttp_web.Response:
...
__all__ = [
"HandlerProtocol",
]

1199
src/python-service/poetry.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,3 @@
[virtualenvs]
create = true
in-project = true

View File

@ -0,0 +1,121 @@
[build-system]
build-backend = "poetry.core.masonry.api"
requires = ["poetry-core>=1.0.0"]
[tool.black]
line-length = 120
target-version = ["py310"]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"raise NotImplementedError",
"if __name__ == .__main__.:",
"class .*Protocol\\):",
"@(abc\\.)?abstractmethod",
"if typing.TYPE_CHECKING:",
]
[tool.coverage.run]
include = ["bin/*", "lib/*"]
[tool.isort]
known_first_party = ["bin", "lib", "tests"]
line_length = 120
profile = "black"
py_version = 310
[tool.poetry]
authors = ["ovsds <ovsds@yandex-team.ru>"]
description = "Python Service Example Backend"
name = "python-service-example-backend"
version = "0.0.1"
[tool.poetry.dependencies]
aiohttp = "3.8.4"
pydantic = {extras = ["dotenv"], version = "1.10.2"}
python = "~3.10"
typing-extensions = "4.5.0"
[tool.poetry.dev-dependencies]
black = "22.6.0"
coverage = "6.5.0"
isort = "5.10.1"
pylint = "2.14.5"
pylint-pydantic = "0.1.4"
pylint-pytest = "1.1.2"
pytest = "7.1.2"
pytest-asyncio = "0.19.0"
pytest-mock = "3.8.2"
pyupgrade = "3.1.0"
sort-all = "1.2.0"
toml-sort = "0.20.0"
[tool.pylint]
disable = [
"broad-except",
"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 = [
"pydantic", # https://github.com/samuelcolvin/pydantic/issues/1961
]
ignore-paths = [
"^.*venv/.*$",
]
load-plugins = [
"pylint_pydantic",
"pylint_pytest",
]
max-args = 10
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 = [
"**/__pycache__",
]
include = [
"bin",
"lib",
"tests",
]
pythonPlatform = "All"
pythonVersion = "3.10"
reportConstantRedefinition = "none"
reportMissingTypeStubs = "warning"
reportPrivateUsage = "information"
reportPropertyTypeMismatch = "warning"
reportUninitializedInstanceVariable = "warning"
reportUnknownMemberType = "none"
reportUnnecessaryTypeIgnoreComment = "warning"
reportUntypedFunctionDecorator = "warning"
typeCheckingMode = "strict"
useLibraryCodeForTypes = true
venv = ".venv"
venvPath = '.'
[tool.pytest.ini_options]
pythonpath = "."
[tool.tomlsort]
all = true
ignore_case = true
in_place = true

View File