diff --git a/CONTRIBUTE.md b/CONTRIBUTE.md
index ccf3cc0..886813d 100644
--- a/CONTRIBUTE.md
+++ b/CONTRIBUTE.md
@@ -70,14 +70,13 @@ urfu-daddy/
│ │ ├── __init__.py # Функция для запуска бота.
│ │ ├── handlers/ # Обработка всех ивентов.
│ │ │ ├── callbacks/ # Обработка запросов.
-│ │ │ ├── commands/ # Обработка комманд.
-│ │ │ ├── utils/ # Вспомогательные компоненты для хендлеров.
-│ │ ├── middlewares/ # Мидлвари для диспетчера.
-│ │ └── services/ # Взаимодействие с другими сервисами бота.
+│ │ │ └── commands/ # Обработка комманд.
+│ │ ├── middlewares/ # Мидлвари для диспетчера/роутеров.
+│ │ ├── services/ # Взаимодействие с другими сервисами бота.
+│ │ └── utils/ # Вспомогательные компоненты для для бота.
│ ├── config/ # Получение env-настроек, настройка логирования.
│ ├── database/ # Инициализация и настройка БД.
-│ ├── locales/ # Папка с локализацией проекта.
-│ └── redis_client/ # Настройка redis-клиента.
+│ └── locales/ # Папка с локализацией проекта.
├── LICENSE # Лицензия проекта.
├── README.md # Описание проекта.
├── CONTRIBUTE.md # Этот файл.
diff --git a/src/bot/__init__.py b/src/bot/__init__.py
index fd3daba..b6edf0c 100644
--- a/src/bot/__init__.py
+++ b/src/bot/__init__.py
@@ -5,7 +5,7 @@ from aiogram import Bot, Dispatcher
from aiogram.fsm.storage.redis import RedisStorage
from loguru import logger as loguru_logger
from redis.asyncio.client import Redis
-from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
+from sqlalchemy.ext.asyncio import async_sessionmaker
from .handlers import include_routers, registry
from .middlewares import connect_middlewares
@@ -14,8 +14,8 @@ from .middlewares import connect_middlewares
@loguru_logger.catch
async def start_bot(
bot_token: str,
- redis_client: Redis,
- database_session: async_sessionmaker[AsyncSession],
+ redis_protocol: Redis,
+ session_maker: async_sessionmaker,
) -> None:
"""
Start Telegram bot.
@@ -23,28 +23,31 @@ async def start_bot(
Args:
bot_token (str): Telegram API bot token.
redis_client (Redis): async configured client for redis.
- database_session (AsyncSession): async database session.
+ session_maker (AsyncSession): async database session maker.
"""
bot = Bot(bot_token)
dispatcher = Dispatcher(
- storage=RedisStorage(redis=redis_client),
+ storage=RedisStorage(redis=redis_protocol),
+ )
+
+ connect_middlewares(
+ dispatcher=dispatcher,
+ session_maker=session_maker,
+ )
+ loguru_logger.debug(
+ "Middlewares have been successfully connected."
)
include_routers(dispatcher)
loguru_logger.debug("Routers have been successfully included.")
- connect_middlewares(dispatcher)
- loguru_logger.debug(
- "Middlewares have been successfully connected."
- )
-
- await bot.delete_webhook(drop_pending_updates=True)
- loguru_logger.debug(
- "All updates for the Telegram bot have been dropped."
- )
-
await registry.set_menu(bot)
loguru_logger.debug("The bot menu has been successfully set.")
+ await bot.delete_webhook(drop_pending_updates=True)
+ loguru_logger.debug(
+ "All updates for the Telegram bot have been dropped.",
+ )
+
await dispatcher.start_polling(bot)
diff --git a/src/bot/handlers/__init__.py b/src/bot/handlers/__init__.py
index da34c81..10b5cfb 100644
--- a/src/bot/handlers/__init__.py
+++ b/src/bot/handlers/__init__.py
@@ -6,7 +6,7 @@ from pathlib import Path
from aiogram import Router
from loguru import logger as loguru_logger
-from .utils.registry import RouterRegistry
+from bot.utils.registry import RouterRegistry
# Main registry router for bot handlers.
registry: RouterRegistry = RouterRegistry()
diff --git a/src/bot/handlers/callbacks/menu.py b/src/bot/handlers/callbacks/menu.py
index ac8ca01..a6a42d9 100644
--- a/src/bot/handlers/callbacks/menu.py
+++ b/src/bot/handlers/callbacks/menu.py
@@ -1,8 +1,8 @@
from aiogram.types import CallbackQuery, Message
from bot.handlers import registry
-from bot.handlers.utils.keyboards import get_menu_markup
-from bot.handlers.utils.types import ChatType
+from bot.utils.keyboards import get_menu_markup
+from bot.utils.types import ChatType
@registry.register(
diff --git a/src/bot/handlers/commands/help.py b/src/bot/handlers/commands/help.py
index ce56a3b..3dadc15 100644
--- a/src/bot/handlers/commands/help.py
+++ b/src/bot/handlers/commands/help.py
@@ -1,7 +1,8 @@
from aiogram.types import Message
from bot.handlers import registry
-from bot.handlers.utils.types import ChatType
+from bot.services.database import check_telegram_user
+from bot.utils.types import ChatType
@registry.register(
@@ -9,5 +10,13 @@ from bot.handlers.utils.types import ChatType
chat_types=ChatType.PRIVATE,
description="Help Command Function Description",
)
-async def cmd_help(message: Message) -> None:
- await message.answer("Help Command Function Answer Text")
+async def cmd_help(message: Message, session) -> None:
+ if message.from_user is None:
+ return
+
+ user = await check_telegram_user(session, message.from_user.id)
+
+ await message.answer(
+ "Help Command Function Answer Text. "
+ f"Your locale is {user.lang}"
+ )
diff --git a/src/bot/handlers/commands/menu.py b/src/bot/handlers/commands/menu.py
index 3df72e9..873c6a3 100644
--- a/src/bot/handlers/commands/menu.py
+++ b/src/bot/handlers/commands/menu.py
@@ -1,8 +1,8 @@
from aiogram.types import Message
from bot.handlers import registry
-from bot.handlers.utils.keyboards import get_menu_markup
-from bot.handlers.utils.types import ChatType
+from bot.utils.keyboards import get_menu_markup
+from bot.utils.types import ChatType
@registry.register(
diff --git a/src/bot/middlewares/__init__.py b/src/bot/middlewares/__init__.py
index c9ca463..be444ef 100644
--- a/src/bot/middlewares/__init__.py
+++ b/src/bot/middlewares/__init__.py
@@ -1,9 +1,16 @@
__all__ = ["connect_middlewares"]
-from aiogram import Router
+from aiogram import Dispatcher
+from sqlalchemy.ext.asyncio import async_sessionmaker
+
+from .database import DatabaseSessionMiddleware
-def connect_middlewares(dispatcher: Router):
- # TODO: Add database and localization middleware and connect them.
- ...
+def connect_middlewares(
+ dispatcher: Dispatcher,
+ session_maker: async_sessionmaker,
+) -> None:
+ database_middleware = DatabaseSessionMiddleware(session_maker)
+
+ dispatcher.update.middleware(database_middleware)
diff --git a/src/bot/middlewares/database.py b/src/bot/middlewares/database.py
new file mode 100644
index 0000000..8b72c58
--- /dev/null
+++ b/src/bot/middlewares/database.py
@@ -0,0 +1,22 @@
+from typing import Any, Awaitable, Callable, Dict
+
+from aiogram import BaseMiddleware
+from aiogram.types import TelegramObject
+from sqlalchemy.ext.asyncio import async_sessionmaker
+
+
+class DatabaseSessionMiddleware(BaseMiddleware):
+ def __init__(self, session_maker: async_sessionmaker):
+ self.session_maker = session_maker
+
+ async def __call__(
+ self,
+ handler: Callable[
+ [TelegramObject, Dict[str, Any]], Awaitable[Any]
+ ],
+ event: TelegramObject,
+ data: Dict[str, Any],
+ ) -> Any:
+ async with self.session_maker() as session:
+ data["session"] = session
+ return await handler(event, data)
diff --git a/src/bot/services/__init__.py b/src/bot/services/__init__.py
index 7dfd4a8..f52f96a 100644
--- a/src/bot/services/__init__.py
+++ b/src/bot/services/__init__.py
@@ -1,5 +1,4 @@
-__all__ = []
+__all__ = ["check_telegram_user"]
-# TODO: Add bot business logic
-# (working with other service components).
+from .database import check_telegram_user
diff --git a/src/bot/services/database.py b/src/bot/services/database.py
new file mode 100644
index 0000000..6472bae
--- /dev/null
+++ b/src/bot/services/database.py
@@ -0,0 +1,52 @@
+from sqlalchemy import select
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from database import TelegramUser
+
+
+async def _get_telegram_user(
+ session: AsyncSession,
+ telegram_id: int,
+) -> TelegramUser | None:
+ """
+ Получить пользователя из БД.
+ Возвращает либо объект пользователя, либо None.
+ """
+ query = select(TelegramUser).where(TelegramUser.id == telegram_id)
+ result = await session.execute(query)
+
+ return result.scalar()
+
+
+async def _add_telegram_user(
+ session: AsyncSession,
+ telegram_id: int,
+) -> TelegramUser:
+ """
+ Добавить пользователя в БД.
+ Возвращает только что добавленный объект пользователя.
+ """
+ user = TelegramUser(id=telegram_id)
+
+ session.add(user)
+
+ await session.commit()
+
+ return user
+
+
+async def check_telegram_user(
+ session: AsyncSession,
+ telegram_id: int,
+) -> TelegramUser:
+ """
+ Проверяет, есть ли пользователь в БД.
+ Если его нет, то добавляет.
+ Всегда возвращает объект пользователя.
+ """
+ result = await _get_telegram_user(session, telegram_id)
+
+ if result is not None:
+ return result
+
+ return await _add_telegram_user(session, telegram_id)
diff --git a/src/bot/services/database/__init__.py b/src/bot/utils/__init__.py
similarity index 100%
rename from src/bot/services/database/__init__.py
rename to src/bot/utils/__init__.py
diff --git a/src/bot/handlers/utils/filters/__init__.py b/src/bot/utils/filters/__init__.py
similarity index 97%
rename from src/bot/handlers/utils/filters/__init__.py
rename to src/bot/utils/filters/__init__.py
index e59a335..c4d3852 100644
--- a/src/bot/handlers/utils/filters/__init__.py
+++ b/src/bot/utils/filters/__init__.py
@@ -6,7 +6,7 @@ from typing import Union
from aiogram.filters import BaseFilter
from aiogram.types import CallbackQuery, Message
-from bot.handlers.utils.types import ChatType
+from bot.utils.types import ChatType
class ChatTypeFilter(BaseFilter):
diff --git a/src/bot/handlers/utils/keyboards/__init__.py b/src/bot/utils/keyboards/__init__.py
similarity index 100%
rename from src/bot/handlers/utils/keyboards/__init__.py
rename to src/bot/utils/keyboards/__init__.py
diff --git a/src/bot/handlers/utils/registry/__init__.py b/src/bot/utils/registry/__init__.py
similarity index 98%
rename from src/bot/handlers/utils/registry/__init__.py
rename to src/bot/utils/registry/__init__.py
index 4854455..ba6307d 100644
--- a/src/bot/handlers/utils/registry/__init__.py
+++ b/src/bot/utils/registry/__init__.py
@@ -13,8 +13,8 @@ from aiogram.types.bot_command_scope_all_private_chats import (
)
from loguru import logger as loguru_logger
-from bot.handlers.utils.filters import ChatTypeFilter
-from bot.handlers.utils.types import (
+from bot.utils.filters import ChatTypeFilter
+from bot.utils.types import (
ChatType,
HandlerMeta,
HandlerType,
diff --git a/src/bot/handlers/utils/states/__init__.py b/src/bot/utils/states/__init__.py
similarity index 100%
rename from src/bot/handlers/utils/states/__init__.py
rename to src/bot/utils/states/__init__.py
diff --git a/src/bot/handlers/utils/types/__init__.py b/src/bot/utils/types/__init__.py
similarity index 100%
rename from src/bot/handlers/utils/types/__init__.py
rename to src/bot/utils/types/__init__.py
diff --git a/src/bot/handlers/utils/types/chat.py b/src/bot/utils/types/chat.py
similarity index 100%
rename from src/bot/handlers/utils/types/chat.py
rename to src/bot/utils/types/chat.py
diff --git a/src/bot/handlers/utils/types/handler.py b/src/bot/utils/types/handler.py
similarity index 94%
rename from src/bot/handlers/utils/types/handler.py
rename to src/bot/utils/types/handler.py
index 34a2c1c..d589bb9 100644
--- a/src/bot/handlers/utils/types/handler.py
+++ b/src/bot/utils/types/handler.py
@@ -1,7 +1,7 @@
from dataclasses import dataclass
from typing import Any, Awaitable, Callable, TypeVar
-from bot.handlers.utils.types import ChatType
+from bot.utils.types import ChatType
HandlerType = TypeVar(
"HandlerType", bound=Callable[[Any], Awaitable[Any]]
diff --git a/src/config/__init__.py b/src/config/__init__.py
index a2811c6..8e8d2da 100644
--- a/src/config/__init__.py
+++ b/src/config/__init__.py
@@ -1,6 +1,7 @@
__all__ = ["settings", "configure_logger"]
-from .utils import Settings, configure_logger
+from .logger import configure_logger
+from .settings import Settings
settings = Settings()
diff --git a/src/config/logger.py b/src/config/logger.py
new file mode 100644
index 0000000..56dc1e7
--- /dev/null
+++ b/src/config/logger.py
@@ -0,0 +1,94 @@
+import logging
+import sys
+from pathlib import Path
+
+from loguru import logger as loguru_logger
+
+# Constant paths for project.
+LOG_DIR = Path("logs")
+LOG_DIR.mkdir(parents=True, exist_ok=True)
+
+# Log format for loguru logger.
+LOG_FORMAT = (
+ "{time:YYYY-MM-DD HH:mm:ss} | "
+ "{level:<8} | "
+ "{process}.{thread.name} | "
+ "{module}.{function}:{line} – "
+ "{message}"
+)
+
+
+class InterceptHandler(logging.Handler):
+ """
+ Class for intercepting logs from logging, including Aiogram.
+ """
+
+ def emit(self, record: logging.LogRecord) -> None:
+ """
+ Try to intercepting logs from logging.
+
+ Args:
+ record (logging.LogRecord): record from logging.
+ """
+ try:
+ level = loguru_logger.level(record.levelname).name
+ except ValueError:
+ level = record.levelno
+
+ frame, depth = logging.currentframe(), 2
+
+ while frame and frame.f_code.co_filename == logging.__file__:
+ frame = frame.f_back
+ depth += 1
+
+ if frame is None:
+ break
+
+ loguru_logger.opt(
+ depth=depth,
+ exception=record.exc_info,
+ ).log(
+ level,
+ record.getMessage(),
+ )
+
+
+def configure_logger(
+ file_log_level: str,
+ listen_logging: bool,
+ console_log_level: str,
+) -> None:
+ """
+ Configure loguru logger.
+
+ Args:
+ file_log_level (str): logging level for the .log file.
+ listen_logging (bool): intercepting logs from logging.
+ console_log_level (str): logging level for the console.
+ """
+ if listen_logging:
+ logging.root.handlers.clear()
+ logging.root.setLevel(logging.DEBUG)
+ logging.root.addHandler(InterceptHandler())
+
+ loguru_logger.remove()
+
+ loguru_logger.add(
+ sys.stdout,
+ level=console_log_level,
+ colorize=True,
+ enqueue=True,
+ format=LOG_FORMAT,
+ )
+
+ loguru_logger.add(
+ LOG_DIR / "{time:YYYY-MM-DD}.log",
+ level=file_log_level,
+ colorize=False,
+ enqueue=True,
+ format=LOG_FORMAT,
+ rotation="10 MB",
+ retention="14 days",
+ compression="zip",
+ serialize=False,
+ )
diff --git a/src/config/utils.py b/src/config/settings.py
similarity index 52%
rename from src/config/utils.py
rename to src/config/settings.py
index 19813a2..6ebcf88 100644
--- a/src/config/utils.py
+++ b/src/config/settings.py
@@ -1,25 +1,9 @@
-import logging
-import sys
from pathlib import Path
from typing import Literal
-from loguru import logger as loguru_logger
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
-# Constant paths for project.
-LOG_DIR = Path("logs")
-LOG_DIR.mkdir(parents=True, exist_ok=True)
-
-# Log format for loguru logger.
-LOG_FORMAT = (
- "{time:YYYY-MM-DD HH:mm:ss} | "
- "{level:<8} | "
- "{process}.{thread.name} | "
- "{module}.{function}:{line} – "
- "{message}"
-)
-
class Settings(BaseSettings):
"""
@@ -40,6 +24,12 @@ class Settings(BaseSettings):
PostgreSQL user password.
postgres_name (str):
PostgreSQL database name.
+ postgres_url (str):database_url
+ Full PostgreSQL database url.
+
+ database_engine_echo (bool):
+ Enable SQLAlchemy engine echo.
+ Defaults to True.
redis_host (str):
Redis host name.
@@ -69,8 +59,10 @@ class Settings(BaseSettings):
postgres_pass: str = Field(frozen=True)
postgres_name: str = Field(frozen=True)
+ database_engine_echo: bool = Field(frozen=True, default=True)
+
@property
- def database_url(self):
+ def postgres_url(self):
"""Get PostgreSQL database url."""
return (
f"postgresql+asyncpg://{self.postgres_user}:"
@@ -113,79 +105,3 @@ class Settings(BaseSettings):
env_file_encoding="utf-8",
case_sensitive=False,
)
-
-
-class InterceptHandler(logging.Handler):
- """
- Class for intercepting logs from logging, including Aiogram.
- """
-
- def emit(self, record: logging.LogRecord) -> None:
- """
- Try to intercepting logs from logging.
-
- Args:
- record (logging.LogRecord): record from logging.
- """
- try:
- level = loguru_logger.level(record.levelname).name
- except ValueError:
- level = record.levelno
-
- frame, depth = logging.currentframe(), 2
-
- while frame and frame.f_code.co_filename == logging.__file__:
- frame = frame.f_back
- depth += 1
-
- if frame is None:
- break
-
- loguru_logger.opt(
- depth=depth,
- exception=record.exc_info,
- ).log(
- level,
- record.getMessage(),
- )
-
-
-def configure_logger(
- file_log_level: str,
- listen_logging: bool,
- console_log_level: str,
-) -> None:
- """
- Configure loguru logger.
-
- Args:
- file_log_level (str): logging level for the .log file.
- listen_logging (bool): intercepting logs from logging.
- console_log_level (str): logging level for the console.
- """
- if listen_logging:
- logging.root.handlers.clear()
- logging.root.setLevel(logging.DEBUG)
- logging.root.addHandler(InterceptHandler())
-
- loguru_logger.remove()
-
- loguru_logger.add(
- sys.stdout,
- level=console_log_level,
- colorize=True,
- enqueue=True,
- format=LOG_FORMAT,
- )
-
- loguru_logger.add(
- LOG_DIR / "{time:YYYY-MM-DD}.log",
- level=file_log_level,
- colorize=False,
- enqueue=True,
- format=LOG_FORMAT,
- rotation="10 MB",
- retention="14 days",
- compression="zip",
- serialize=False,
- )
diff --git a/src/database/__init__.py b/src/database/__init__.py
index 4c8fbb8..b6da8ee 100644
--- a/src/database/__init__.py
+++ b/src/database/__init__.py
@@ -1,4 +1,20 @@
-__all__ = ["create_session", "create_db", "drop_db"]
+__all__ = [
+ "Student",
+ "TelegramUser",
+ "UniversityMember",
+ "create_db",
+ "drop_db",
+ "engine",
+ "session_maker",
+ "redis_protocol",
+]
-from .session import create_db, create_session, drop_db
+from .models import Student, TelegramUser, UniversityMember
+from .postgres import (
+ create_db,
+ drop_db,
+ engine,
+ session_maker,
+)
+from .redis import protocol as redis_protocol
diff --git a/src/database/models.py b/src/database/models.py
index cad8f11..b76f0bf 100644
--- a/src/database/models.py
+++ b/src/database/models.py
@@ -84,11 +84,6 @@ class TelegramUser(Base):
autoincrement=False,
)
- username: Mapped[str] = mapped_column(
- String(64),
- nullable=True,
- )
-
lang: Mapped[str] = mapped_column(
String(2),
default="ru",
diff --git a/src/database/postgres.py b/src/database/postgres.py
new file mode 100644
index 0000000..a2bc826
--- /dev/null
+++ b/src/database/postgres.py
@@ -0,0 +1,32 @@
+# from loguru import logger as loguru_logger
+from sqlalchemy.ext.asyncio import (
+ AsyncEngine,
+ AsyncSession,
+ async_sessionmaker,
+ create_async_engine,
+)
+
+from config import settings
+
+from .models import Base
+
+engine = create_async_engine(
+ url=settings.postgres_url,
+ echo=settings.database_engine_echo,
+)
+
+session_maker = async_sessionmaker(
+ bind=engine,
+ class_=AsyncSession,
+ expire_on_commit=False,
+)
+
+
+async def create_db(engine: AsyncEngine):
+ async with engine.begin() as conn:
+ await conn.run_sync(Base.metadata.create_all)
+
+
+async def drop_db(engine: AsyncEngine):
+ async with engine.begin() as conn:
+ await conn.run_sync(Base.metadata.drop_all)
diff --git a/src/database/redis.py b/src/database/redis.py
new file mode 100644
index 0000000..2bcebff
--- /dev/null
+++ b/src/database/redis.py
@@ -0,0 +1,8 @@
+from redis.asyncio.client import Redis
+
+from config import settings
+
+protocol = Redis(
+ host=settings.redis_host,
+ port=settings.redis_port,
+)
diff --git a/src/database/session.py b/src/database/session.py
deleted file mode 100644
index 4fd389c..0000000
--- a/src/database/session.py
+++ /dev/null
@@ -1,40 +0,0 @@
-# from loguru import logger as loguru_logger
-from sqlalchemy.ext.asyncio import (
- AsyncEngine,
- AsyncSession,
- async_sessionmaker,
- create_async_engine,
-)
-
-from .models import Base
-
-engine: AsyncEngine
-
-
-def create_session(
- url: str, engine_echo: bool = True
-) -> async_sessionmaker[AsyncSession]:
- global engine
- engine = create_async_engine(url=url, echo=engine_echo)
-
- session = async_sessionmaker(
- bind=engine,
- class_=AsyncSession,
- expire_on_commit=False,
- )
-
- return session
-
-
-async def create_db():
- global engine
-
- async with engine.begin() as conn:
- await conn.run_sync(Base.metadata.create_all)
-
-
-async def drop_db():
- global engine
-
- async with engine.begin() as conn:
- await conn.run_sync(Base.metadata.drop_all)
diff --git a/src/main.py b/src/main.py
index 17de15b..4e1251a 100644
--- a/src/main.py
+++ b/src/main.py
@@ -2,11 +2,10 @@ import asyncio
from bot import start_bot
from config import configure_logger, settings
-from database import create_db, create_session
-from redis_client import get_redis_client
+from database import create_db, engine, redis_protocol, session_maker
-def main() -> None:
+async def main() -> None:
"""
Launch of all service components.
Configure logger and start bot.
@@ -17,26 +16,15 @@ def main() -> None:
listen_logging=settings.listen_logging,
)
- session = create_session(
- url=settings.database_url, engine_echo=True
- )
-
# FIXME: Add argument for create/drop db.
- asyncio.run(create_db())
+ await create_db(engine)
- redis_client = get_redis_client(
- host=settings.redis_host,
- port=settings.redis_port,
- )
-
- asyncio.run(
- start_bot(
- bot_token=settings.bot_token,
- redis_client=redis_client,
- database_session=session,
- )
+ await start_bot(
+ bot_token=settings.bot_token,
+ redis_protocol=redis_protocol,
+ session_maker=session_maker,
)
if __name__ == "__main__":
- main()
+ asyncio.run(main())
diff --git a/src/redis_client/__init__.py b/src/redis_client/__init__.py
deleted file mode 100644
index f2425d4..0000000
--- a/src/redis_client/__init__.py
+++ /dev/null
@@ -1,23 +0,0 @@
-__all__ = ["get_redis_client"]
-
-
-from redis.asyncio.client import Redis
-
-
-def get_redis_client(host: str, port: int) -> Redis:
- """
- Get async redis client.
-
- Args:
- host (str): redis host.
- port (int): redis port.
-
- Returns:
- Redis: async redis client.
- """
- client = Redis(
- host=host,
- port=port,
- )
-
- return client