add tests and docker infra

This commit is contained in:
ivankirpichnikov
2025-10-17 02:21:46 +03:00
parent 31d06fc0b4
commit 45d7926af1
24 changed files with 806 additions and 24 deletions
BIN
View File
Binary file not shown.
+6
View File
@@ -0,0 +1,6 @@
config.toml
.venv
.idea
__pycache__
docker-compose.yml
*.egg-info
+85
View File
@@ -0,0 +1,85 @@
[doc("Все команды")]
default:
just --list --unsorted --list-heading $'Commands…\n'
# =========
# > Docker
# =========
[no-cd]
[group("Docker")]
[doc("Билд основного контейнера")]
docker-build-main:
docker build -t template_project_deploy .
[no-cd]
[group("Docker")]
[doc("Билд тестового контейнера")]
docker-build-tests:
docker build -t template_project_tests .
[no-cd]
[group("Docker")]
[doc("Билд миграционного контейнера")]
docker-build-migrations:
docker build -t template_project_migrations .
[no-cd]
[group("Docker")]
[doc("Билд всех контейнеров")]
docker-build-all:
just docker-build-main
just docker-build-tests
just docker-build-migrations
[no-cd]
[group("Docker")]
[doc("Запуск композа")]
docker-up:
just docker-build-all
docker compose up web_api -d
# =========
# > Tests
# =========
[no-cd]
[group("Tests")]
[doc("Запуск тестов")]
tests-run:
just docker-up
docker compose up tests --abort-on-container-exit --remove-orphans
coverage report
# =========
# > Lints
# =========
[no-cd]
[group("Lints")]
[doc("Запуск всех линтов")]
lints-run:
ruff check
mypy
codespell src tests
bandit src tests
# =========
# > Migrations
# =========
[no-cd]
[group("Migrations")]
[doc("Запуск миграции")]
migrations-run tag="head":
docker compose run --remove-orphans migrations alembic upgrade {{tag}}
docker compose down postgresql
[no-cd]
[group("Migrations")]
[doc('Создание миграции')]
migrations-make message:
docker compose run migrations alembic revision --autogenerate -m "{{message}}"
docker compose down postgresql
+63 -6
View File
@@ -1,13 +1,70 @@
FROM python:3.13-alpine
# =====
# > Python-Base
# =====
WORKDIR /app
FROM python:3.13-slim-bookworm AS python-base
RUN pip install uv
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONOPTIMIZE=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1 \
PIP_DEFAULT_TIMEOUT=100 \
APP_PATH="/app" \
UV_VERSION="0.8.19"
ENV \
VIRTUAL_ENV="$APP_PATH/.venv" \
PATH="$VIRTUAL_ENV/bin:$PATH" \
PROJECT_PATH="$APP_PATH/src/template_project"
WORKDIR $APP_PATH
# =====
# > Builder
# =====
FROM python-base AS template_project_builder
RUN apt-get update \
&& apt-get install -y --no-install-recommends gcc git \
&& rm -rf /var/lib/apt/lists
RUN pip install --no-cache-dir "uv==$UV_VERSION"
RUN mkdir -p ./src/
COPY pyproject.toml /app/pyproject.toml
COPY pyproject.toml $APP_PATH/pyproject.toml
RUN uv venv -p 3.13 && uv pip install -e .
COPY ./src/template_project $PROJECT_PATH
RUN uv pip install -e . --system --no-cache
# =====
# > Deploy
# =====
COPY ./src/template_project /app/src/template_project
FROM template_project_builder AS template_project_deploy
COPY --from=template_project_builder $VIRTUAL_ENV $VIRTUAL_ENV
# =====
# > Tests
# =====
FROM template_project_builder AS template_project_tests
COPY --from=template_project_builder $VIRTUAL_ENV $VIRTUAL_ENV
RUN uv pip install --group tests
COPY ./tests $APP_PATH/tests
# =====
# > Migrations
# =====
FROM template_project_builder AS template_project_migrations
COPY --from=template_project_builder $VIRTUAL_ENV $VIRTUAL_ENV
VOLUME $PROJECT_PATH/migrations
RUN uv pip install --group migrations
+147
View File
@@ -0,0 +1,147 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts.
# this is typically a path given in POSIX (e.g. forward slashes)
# format, relative to the token %(here)s which refers to the location of this
# ini file
script_location = %(here)s/src/template_project/migrations
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
# for all available tokens
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory. for multiple paths, the path separator
# is defined by "path_separator" below.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the tzdata library which can be installed by adding
# `alembic[tz]` to the pip requirements.
# string value is passed to ZoneInfo()
# leave blank for localtime
# timezone =
# max length of characters to apply to the "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; This defaults
# to <script_location>/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "path_separator"
# below.
# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions
# path_separator; This indicates what character is used to split lists of file
# paths, including version_locations and prepend_sys_path within configparser
# files such as alembic.ini.
# The default rendered in new alembic.ini files is "os", which uses os.pathsep
# to provide os-dependent path splitting.
#
# Note that in order to support legacy alembic.ini files, this default does NOT
# take place if path_separator is not present in alembic.ini. If this
# option is omitted entirely, fallback logic is as follows:
#
# 1. Parsing of the version_locations option falls back to using the legacy
# "version_path_separator" key, which if absent then falls back to the legacy
# behavior of splitting on spaces and/or commas.
# 2. Parsing of the prepend_sys_path option falls back to the legacy
# behavior of splitting on spaces, commas, or colons.
#
# Valid values for path_separator are:
#
# path_separator = :
# path_separator = ;
# path_separator = space
# path_separator = newline
#
# Use os.pathsep. Default configuration used for new projects.
path_separator = os
# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
# database URL. This is consumed by the user-maintained env.py script only.
# other means of configuring database URLs may be customized within the env.py
# file.
sqlalchemy.url = driver://user:pass@localhost/dbname
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module
# hooks = ruff
# ruff.type = module
# ruff.module = ruff
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
# Alternatively, use the exec runner to execute a binary found on your PATH
# hooks = ruff
# ruff.type = exec
# ruff.executable = ruff
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
# Logging configuration. This is also consumed by the user-maintained
# env.py script only.
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARNING
handlers = console
qualname =
[logger_sqlalchemy]
level = WARNING
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
+60
View File
@@ -0,0 +1,60 @@
services:
web_api:
image: template_project_deploy
build: .
restart: unless-stopped
volumes:
- ./config.toml:/app/config.toml:ro
command: web_api_cli /app/config.toml
depends_on:
- postgresql
expose:
- "8080"
ports:
- "8080:8080"
healthcheck:
test: [ "CMD", "curl", "-f", "http://localhost:8080/healthcheck/" ]
interval: 2s
tests:
image: template_project_tests
volumes:
- ./cov/:/app/cov
- ./tests:/app/tests:ro
- ./config.toml:/app/config.toml:ro
environment:
CONFIGURATION_PATH: /app/config.toml
command: coverage run --source src -m pytest && coverage report && coverage xml -o ./cov/coverage.xml
depends_on:
web_api:
condition: service_healthy
migrations:
image: template_project_migrations
environment:
CONFIGURATION_PATH: /app/config.toml
volumes:
- ./alembic.ini:/app/alembic.ini:ro
- ./config.toml:/app/config.toml:ro
- ./src/template_project/migrations:/app/src/template_project/migrations
depends_on:
- postgresql
postgresql:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_PASSWORD: template_project_password
POSTGRES_USER: template_project_user
POSTGRES_DB: template_project_db
PGDATA: /var/lib/postgresql/data/
expose:
- "5432"
ports:
- "127.0.0.1:5432:5432"
volumes:
- postgresql_volume:/var/lib/postgresql/data
volumes:
redis_volume:
postgresql_volume:
+147 -2
View File
@@ -4,11 +4,156 @@ requires-python = ">=3.13"
description = "template project"
version = "1.0.0"
dependencies = [
"uuid-utils==0.11.1",
"uuid_utils==0.11.1",
"adaptix==3.0.0b11",
"fastapi==0.119.0",
"uvicorn==0.37.0",
"sqlalchemy==2.0.44",
"dishka==1.7.2",
"argon2-cffi==23.1.0",
"argon2_cffi==23.1.0",
"cryptography==46.0.3",
"httpx==0.28.1",
]
[dependency-groups]
types = [
"types-cachetools==6.2.0.20250827",
]
migrations = [
"alembic==1.17.0",
]
linters = [
"mypy==1.17.1",
"ruff==0.12.11",
"codespell==2.4.1",
"bandit==1.8.6",
{ include-group = "types" },
]
tests = [
"pytest==8.4.0",
"coverage==7.11.0",
"pytest_asyncio==1.2.0",
]
dev = [
{ include-group = "tests" },
{ include-group = "linters" },
{ include-group = "migrations"},
]
[project.scripts]
web_api_cli = "template_project.web_api.entry_point:main"
[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
filterwarnings = [
"ignore::UserWarning",
'ignore:function ham\(\) is deprecated:DeprecationWarning',
]
[tool.mypy]
strict = true
strict_bytes = true
local_partial_types = true
warn_unreachable = true
files = ["src", "tests"]
exclude = [
"src/template_project/migrations",
]
enable_error_code = [
"truthy-bool",
"truthy-iterable",
"redundant-expr",
"unused-awaitable",
"ignore-without-code",
"possibly-undefined",
"redundant-self",
"explicit-override",
"mutable-override",
"unimported-reveal",
"deprecated",
]
[tool.ruff]
fix = true
preview = true
target-version = "py313"
line-length = 120
indent-width = 4
exclude = [
"src/template_project/migrations/**.py",
]
[tool.ruff.lint]
select = [
"A", # flake8-builtins
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"C90", # maccabe
"COM", # flake8-commas
"D", # pydocstyle
"DTZ", # flake8-datetimez
"E", # pycodestyle
"ERA", # flake8-eradicate
"EXE", # flake8-executable
"F", # pyflakes
"FA", # flake8-future-annotations
"FBT", # flake8-boolean-trap
"FLY", # pyflint
"FURB", # refurb
"G", # flake8-logging-format
"I", # isort
"ICN", # flake8-import-conventions
"ISC", # flake8-implicit-str-concat
"LOG", # flake8-logging
"N", # pep8-naming
"PERF", # perflint
"PIE", # flake8-pie
"PL", # pylint
"PT", # flake8-pytest-style
"PTH", # flake8-use-pathlib
"Q", # flake8-quotes
"RET", # flake8-return
"RSE", # flake8-raise
"RUF", # ruff
"S", # flake8-bandit
"SIM", # flake8-simpify
"SLF", # flake8-self
"SLOT", # flake8-slots
"T100", # flake8-debugger
"TRY", # tryceratops
"UP", # pyupgrade
"W", # pycodestyle
"YTT", # flake8-2020
]
ignore = [
"A005", # allow to shadow stdlib and builtin module names
"COM812", # trailing comma, conflicts with `ruff format`
# Different doc rules that we don't really care about:
"D",
"ISC001", # implicit string concat conflicts with `ruff format`
"ISC003", # prefer explicit string concat over implicit concat
"PLR09", # we have our own complexity rules
"PLR2004", # do not report magic numbers
"PLR6301", # do not require classmethod / staticmethod when self not used
"TRY003", # long exception messages from `tryceratops`
]
external = [ "WPS" ]
[tool.ruff.lint.per-file-ignores]
"tests/*.py" = [
"S101", # asserts
]
[tool.ruff.lint.isort]
case-sensitive = true
combine-as-imports = true
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = false
line-ending = "lf"
# This is only required because we have invalid on-purpose code in docstrings:
docstring-code-format = false
@@ -1,7 +1,7 @@
__all__ = (
"access_token_table",
"meta_data",
"user_table",
"access_token_table",
)
from sqlalchemy import UUID, Boolean, Column, DateTime, ForeignKey, MetaData, String, Table
@@ -1,12 +1,14 @@
from typing import override
from typing import TYPE_CHECKING, override
from template_project.application.access_token.entity import AccessTokenId
from template_project.application.common.errors import ApplicationError, to_error
if TYPE_CHECKING:
from template_project.application.access_token.entity import AccessTokenId
@to_error
class AccessTokenExpiredError(ApplicationError):
id_: AccessTokenId
id_: "AccessTokenId"
@override
def __str__(self) -> str:
+1
View File
@@ -0,0 +1 @@
Generic single-database configuration.
+86
View File
@@ -0,0 +1,86 @@
from logging.config import fileConfig
import os
from pathlib import Path
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
from template_project.web_api.configuration import load_configuration
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
from template_project.adapters.data_gateways.tables import meta_data
target_metadata = meta_data
configuration = load_configuration(Path(os.environ["CONFIGURATION_PATH"]))
config.set_main_option("sqlalchemy.url", configuration.database.url.get_value())
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection, target_metadata=target_metadata
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
@@ -0,0 +1,28 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
def upgrade() -> None:
"""Upgrade schema."""
${upgrades if upgrades else "pass"}
def downgrade() -> None:
"""Downgrade schema."""
${downgrades if downgrades else "pass"}
@@ -31,6 +31,10 @@ class ServerConfiguration:
port: int
access_log: bool
@property
def url(self) -> str:
return f"http://{self.host}:{self.port}"
@to_configuration
class Configuration:
@@ -1,5 +1,6 @@
import argparse
import asyncio
import os
import sys
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
@@ -14,6 +15,7 @@ from fastapi.middleware.cors import CORSMiddleware
from template_project.web_api.configuration import load_configuration
from template_project.web_api.ioc.make import make_ioc
from template_project.web_api.routes import healthcheck, user
LOG_CONFIG: Final = {
"version": 1,
@@ -53,15 +55,15 @@ def make_asgi_application(
version="1.0.0",
openapi_url="/openapi.json",
)
origins = ["*"]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(user.router)
app.include_router(healthcheck.router)
setup_dishka(container=ioc, app=app)
@@ -91,13 +93,17 @@ def main() -> None:
asyncio.set_event_loop_policy(WindowsSelectorEventLoopPolicy())
arg_parser = argparse.ArgumentParser()
subparsers = arg_parser.add_subparsers()
web_api_parser = subparsers.add_parser("web_api")
web_api_parser.add_argument("configuration", dest="configuration", type=Path)
arg_parser.add_argument("configuration", default=None)
args = arg_parser.parse_args()
_main(args.configuration)
configuration_path = args.configuration or os.getenv("CONFIGURATION_PATH")
if configuration_path is None:
raise RuntimeError(
"pass the path to the config or specify it in the environment variables `CONFIGURATION_PATH`",
)
_main(Path(configuration_path))
if __name__ == "__main__":
@@ -8,13 +8,13 @@ from template_project.web_api.configuration import DatabaseConfiguration
class ConnectionProvider(Provider):
@provide(scope=Scope.APP)
async def make_engine(self, configuration: DatabaseConfiguration) -> AsyncIterable[AsyncEngine]:
async def engine(self, configuration: DatabaseConfiguration) -> AsyncIterable[AsyncEngine]:
engine = create_async_engine(configuration.url.get_value())
yield engine
await engine.dispose()
@provide()
async def make_connection(self, engine: AsyncEngine) -> AsyncIterable[AsyncSession]:
@provide(scope=Scope.REQUEST)
async def async_session(self, engine: AsyncEngine) -> AsyncIterable[AsyncSession]:
session = AsyncSession(
bind=engine,
expire_on_commit=True,
+9
View File
@@ -0,0 +1,9 @@
from dishka import BaseScope, Provider, Scope, provide
from template_project.web_api.identity_provider import WebApiIdentityProvider
class IdPProvider(Provider):
scope: BaseScope | None = Scope.REQUEST
web_api = provide(WebApiIdentityProvider)
+6 -1
View File
@@ -1,4 +1,4 @@
from dishka import AsyncContainer, make_async_container
from dishka import STRICT_VALIDATION, AsyncContainer, make_async_container
from dishka.integrations.fastapi import FastapiProvider
from template_project.web_api.configuration import (
@@ -7,19 +7,24 @@ from template_project.web_api.configuration import (
DatabaseConfiguration,
ServerConfiguration,
)
from template_project.web_api.ioc.connection import ConnectionProvider
from template_project.web_api.ioc.cryptographer import CryptographerProvider
from template_project.web_api.ioc.data_gateway import DataGatewayProvider
from template_project.web_api.ioc.factory import FactoryProvider
from template_project.web_api.ioc.idp import IdPProvider
from template_project.web_api.ioc.interactor import InteractorProvider
def make_ioc(configuration: Configuration) -> AsyncContainer:
return make_async_container(
IdPProvider(),
FactoryProvider(),
FastapiProvider(),
ConnectionProvider(),
InteractorProvider(),
DataGatewayProvider(),
CryptographerProvider(),
validation_settings=STRICT_VALIDATION,
context={
ServerConfiguration: configuration.server,
DatabaseConfiguration: configuration.database,
@@ -0,0 +1,14 @@
from dishka.integrations.fastapi import DishkaRoute
from fastapi import APIRouter
from pydantic import BaseModel
router = APIRouter(route_class=DishkaRoute)
class HealthcheckResponse(BaseModel):
ok: bool
@router.get("/healthcheck")
async def healthcheck() -> HealthcheckResponse:
return HealthcheckResponse(ok=True)
View File
View File
+17
View File
@@ -0,0 +1,17 @@
from collections.abc import AsyncIterable
from pathlib import Path
import pytest
from dishka import AsyncContainer
from template_project.web_api.configuration import load_configuration
from tests.web_api.ioc import make_ioc
@pytest.fixture
async def dishka_container() -> AsyncIterable[AsyncContainer]:
path = Path("config.toml")
configuration = load_configuration(path)
ioc = make_ioc(configuration)
yield ioc
await ioc.close()
View File
+14
View File
@@ -0,0 +1,14 @@
from dishka import FromDishka
from httpx import AsyncClient
from tests.web_api.ioc import inject
@inject
async def test_healthcheck(
http_client: FromDishka[AsyncClient],
) -> None:
response = await http_client.get("/healthcheck")
response_json = response.json()
assert response_json["ok"]
+96
View File
@@ -0,0 +1,96 @@
from collections.abc import Callable
from inspect import Parameter
from typing import Final
from dishka import STRICT_VALIDATION, AsyncContainer, BaseScope, Provider, Scope, make_async_container, provide
from dishka.integrations.base import wrap_injection
from dishka.integrations.fastapi import FastapiProvider
from httpx import AsyncClient
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession
from template_project.web_api.configuration import (
AccessTokenConfiguration,
Configuration,
DatabaseConfiguration,
ServerConfiguration,
)
from template_project.web_api.ioc.connection import ConnectionProvider
from template_project.web_api.ioc.cryptographer import CryptographerProvider
from template_project.web_api.ioc.data_gateway import DataGatewayProvider
from template_project.web_api.ioc.factory import FactoryProvider
from template_project.web_api.ioc.idp import IdPProvider
from template_project.web_api.ioc.interactor import InteractorProvider
class DatabaseClearer:
def __init__(self, session: AsyncSession) -> None:
self._session = session
async def clear(self) -> None:
await self._session.execute(
text("""
DO $$
DECLARE
tb text;
BEGIN
FOR tb IN (
SELECT tablename
FROM pg_catalog.pg_tables
WHERE schemaname = 'public'
AND tablename != 'alembic_version'
)
LOOP
EXECUTE 'TRUNCATE TABLE ' || tb || ' CASCADE';
END LOOP;
END $$;
"""),
)
class TestProvider(Provider):
scope: BaseScope | None = Scope.REQUEST
database_clearer = provide(DatabaseClearer)
@provide
def http_client(self) -> AsyncClient:
return AsyncClient(base_url="http://web_api:8080")
def make_ioc(configuration: Configuration) -> AsyncContainer:
return make_async_container(
IdPProvider(),
FactoryProvider(),
FastapiProvider(),
ConnectionProvider(),
InteractorProvider(),
DataGatewayProvider(),
CryptographerProvider(),
TestProvider(),
validation_settings=STRICT_VALIDATION,
context={
ServerConfiguration: configuration.server,
DatabaseConfiguration: configuration.database,
AccessTokenConfiguration: configuration.access_token,
},
)
CONTAINER_PARAM: Final = "dishka_container"
def inject[ReturnT, **FuncParams](func: Callable[FuncParams, ReturnT]) -> Callable[FuncParams, ReturnT]:
return wrap_injection(
func=func,
is_async=True,
manage_scope=True,
container_getter=lambda args, kwargs: kwargs[CONTAINER_PARAM],
additional_params=[
Parameter(
name=CONTAINER_PARAM,
annotation=AsyncContainer,
kind=Parameter.KEYWORD_ONLY,
),
],
)