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

Original code by Dmitriy

This commit is contained in:
Artem Litvinov 2023-09-16 10:44:26 +01:00
parent 848e7f059f
commit 397c50ebea
31 changed files with 1826 additions and 0 deletions

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

114
src/python-service/Makefile Normal file
View File

@ -0,0 +1,114 @@
IMAGE_REGISTRY=ghcr.io/yp-middle-python-24
IMAGE_REPOSITORY=python-service-example-backend
PENV = .venv
NENV = ../node_modules
PYTHON = $(PENV)/bin/python
PYRIGHT = $(NENV)/.bin/pyright
TOML_SORT = $(PENV)/bin/toml-sort
PROJECT_FOLDERS = bin lib tests
.PHONY: init
init:
@echo 'Installing python dependencies...'
@poetry install
.PHONY: lint
lint:
@echo 'Running poetry checks...'
@poetry check
@echo 'Running black checks...'
@$(PYTHON) -m black --check .
@echo 'Running isort checks...'
@$(PYTHON) -m isort --check .
@echo 'Running toml-sort checks...'
@$(TOML_SORT) --check poetry.toml pyproject.toml
@echo ''
@echo 'Running pylint checks...'
@$(PYTHON) -m pylint .
@echo 'Running pyright checks...'
@$(PYRIGHT)
.PHONY: lint-fix
lint-fix:
@echo 'Running poetry autofixes...'
@poetry check
@poetry lock --no-update
@echo 'Running black autofixes...'
@$(PYTHON) -m black --safe .
@echo 'Running isort autofixes...'
@$(PYTHON) -m isort --atomic .
@echo 'Running toml-sort autofixes...'
@$(TOML_SORT) --in-place poetry.toml pyproject.toml
@echo 'Running sort-all autofixes...'
@find $(PROJECT_FOLDERS) -name '*.py' -type f -exec $(PYTHON) -m sort_all '{}' \;
@echo 'Running pyupgrade autofixes...'
@find $(PROJECT_FOLDERS) -name '*.py' -type f -exec $(PYTHON) -m pyupgrade --py310-plus '{}' \;
@echo 'Running pylint checks...'
@$(PYTHON) -m pylint .
@echo 'Running pyright checks...'
@$(PYRIGHT)
.PHONY: test
test:
@echo 'Running tests...'
@#$(PYTHON) -m pytest tests
.PHONY: clean
clean:
@echo 'Cleaning python dependencies...'
@rm -rf $(PENV)
@echo 'Cleaning pytest cache...'
@rm -rf .pytest_cache
@echo 'Cleaning coverage results...'
@rm -rf .coverage
@rm -rf htmlcov
# Local development
.PHONY: dev-server-start
dev-server-start:
@$(PYTHON) -m bin.main
# Test coverage
.PHONY: test-coverage-run
test-coverage-run:
@echo 'Running test coverage...'
@$(PYTHON) -m coverage run -m pytest tests
.PHONY: test-coverage-report
test-coverage-report:
@$(PYTHON) -m coverage report -m
.PHONY: test-coverage-html
test-coverage-html:
@$(PYTHON) -m coverage html
@$(PYTHON) -m webbrowser -t htmlcov/index.html
# CI-specific
.PHONY: ci-image-build
ci-image-build:
@echo 'Building image...'
@docker build --tag $(IMAGE_REGISTRY)/$(IMAGE_REPOSITORY):$(IMAGE_TAG) .
.PHONY: ci-image-push
ci-image-push:
@echo 'Uploading image...'
@docker push $(IMAGE_REGISTRY)/$(IMAGE_REPOSITORY):$(IMAGE_TAG)

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",
]

1224
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