mirror of
https://github.com/ijaric/voice_assistant.git
synced 2025-05-24 14:33:26 +00:00
Merge pull request #2 from ijaric/feature/python-service
Python Service Template [Test]
This commit is contained in:
commit
b606a21951
2
src/python-service/.dockerignore
Normal file
2
src/python-service/.dockerignore
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
.venv/
|
||||||
|
tests/
|
13
src/python-service/.gitignore
vendored
Normal file
13
src/python-service/.gitignore
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# Python dependencies
|
||||||
|
.venv/
|
||||||
|
|
||||||
|
# Python cache
|
||||||
|
__pycache__/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Coverage reports
|
||||||
|
/.coverage
|
||||||
|
htmlcov/
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
11
src/python-service/.lintstagedrc.json
Normal file
11
src/python-service/.lintstagedrc.json
Normal 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"]
|
||||||
|
}
|
12
src/python-service/.prettierignore
Normal file
12
src/python-service/.prettierignore
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# Python dependencies
|
||||||
|
.venv/
|
||||||
|
|
||||||
|
# Python cache
|
||||||
|
__pycache__/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Coverage reports
|
||||||
|
/.coverage
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
19
src/python-service/Dockerfile
Normal file
19
src/python-service/Dockerfile
Normal 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"]
|
3
src/python-service/Makefile
Normal file
3
src/python-service/Makefile
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
include ../../common_makefile.mk
|
||||||
|
|
||||||
|
PROJECT_FOLDERS = bin lib tests
|
28
src/python-service/README.md
Normal file
28
src/python-service/README.md
Normal 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
|
0
src/python-service/bin/__init__.py
Normal file
0
src/python-service/bin/__init__.py
Normal file
0
src/python-service/bin/main/__init__.py
Normal file
0
src/python-service/bin/main/__init__.py
Normal file
37
src/python-service/bin/main/__main__.py
Normal file
37
src/python-service/bin/main/__main__.py
Normal 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()
|
0
src/python-service/lib/__init__.py
Normal file
0
src/python-service/lib/__init__.py
Normal file
0
src/python-service/lib/api/__init__.py
Normal file
0
src/python-service/lib/api/__init__.py
Normal file
0
src/python-service/lib/api/rest/__init__.py
Normal file
0
src/python-service/lib/api/rest/__init__.py
Normal file
0
src/python-service/lib/api/rest/v1/__init__.py
Normal file
0
src/python-service/lib/api/rest/v1/__init__.py
Normal file
5
src/python-service/lib/api/rest/v1/health/__init__.py
Normal file
5
src/python-service/lib/api/rest/v1/health/__init__.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from .liveness_probe import LivenessProbeHandler
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"LivenessProbeHandler",
|
||||||
|
]
|
18
src/python-service/lib/api/rest/v1/health/liveness_probe.py
Normal file
18
src/python-service/lib/api/rest/v1/health/liveness_probe.py
Normal 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",
|
||||||
|
]
|
11
src/python-service/lib/app/__init__.py
Normal file
11
src/python-service/lib/app/__init__.py
Normal 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",
|
||||||
|
]
|
131
src/python-service/lib/app/app.py
Normal file
131
src/python-service/lib/app/app.py
Normal 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",
|
||||||
|
]
|
22
src/python-service/lib/app/errors.py
Normal file
22
src/python-service/lib/app/errors.py
Normal 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",
|
||||||
|
]
|
32
src/python-service/lib/app/settings.py
Normal file
32
src/python-service/lib/app/settings.py
Normal 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",
|
||||||
|
]
|
0
src/python-service/lib/utils/__init__.py
Normal file
0
src/python-service/lib/utils/__init__.py
Normal file
5
src/python-service/lib/utils/aiohttp/__init__.py
Normal file
5
src/python-service/lib/utils/aiohttp/__init__.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from .types import HandlerProtocol
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"HandlerProtocol",
|
||||||
|
]
|
5
src/python-service/lib/utils/aiohttp/types/__init__.py
Normal file
5
src/python-service/lib/utils/aiohttp/types/__init__.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from .handler import HandlerProtocol
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"HandlerProtocol",
|
||||||
|
]
|
13
src/python-service/lib/utils/aiohttp/types/handler.py
Normal file
13
src/python-service/lib/utils/aiohttp/types/handler.py
Normal 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
1199
src/python-service/poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
3
src/python-service/poetry.toml
Normal file
3
src/python-service/poetry.toml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[virtualenvs]
|
||||||
|
create = true
|
||||||
|
in-project = true
|
121
src/python-service/pyproject.toml
Normal file
121
src/python-service/pyproject.toml
Normal 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
|
0
src/python-service/tests/__init__.py
Normal file
0
src/python-service/tests/__init__.py
Normal file
0
src/python-service/tests/integration/__init__.py
Normal file
0
src/python-service/tests/integration/__init__.py
Normal file
0
src/python-service/tests/unit/__init__.py
Normal file
0
src/python-service/tests/unit/__init__.py
Normal file
0
src/python-service/tests/utils/__init__.py
Normal file
0
src/python-service/tests/utils/__init__.py
Normal file
Loading…
Reference in New Issue
Block a user