From 23031b07779868596385197d70a3e75cd09d067e Mon Sep 17 00:00:00 2001
From: Dmitry Afanasyev <71835315+Balshgit@users.noreply.github.com>
Date: Sat, 7 Oct 2023 00:04:12 +0300
Subject: [PATCH] add database and migration logic (#27)
* update chat_microservice
* reformat logger_conf
* add database
* add service and repository logic
* fix constants gpt base url
* add models endpoints
---
README.md | 41 +-
bot_microservice/alembic.ini | 52 ++
bot_microservice/api/bot/controllers.py | 87 ++-
bot_microservice/api/bot/deps.py | 13 -
bot_microservice/api/bot/serializers.py | 24 +
bot_microservice/api/deps.py | 50 ++
bot_microservice/api/system/controllers.py | 7 +-
bot_microservice/constants.py | 34 +-
bot_microservice/core/bot/app.py | 7 +-
bot_microservice/core/bot/commands.py | 6 +-
bot_microservice/core/bot/models/__init__.py | 0
bot_microservice/core/bot/models/chat_gpt.py | 12 +
bot_microservice/core/bot/repository.py | 106 +++
bot_microservice/core/bot/services.py | 114 ++--
bot_microservice/core/lifetime.py | 73 +++
bot_microservice/core/utils.py | 7 +
bot_microservice/infra/database/__init__.py | 0
bot_microservice/infra/database/base.py | 34 +
bot_microservice/infra/database/db_adapter.py | 102 +++
bot_microservice/infra/database/deps.py | 20 +
bot_microservice/infra/database/meta.py | 3 +
.../infra/database/migrations/__init__.py | 1 +
.../infra/database/migrations/env.py | 81 +++
.../infra/database/migrations/script.py.mako | 24 +
.../versions/2023-10-05-18-28_eb78565abec7.py | 34 +
.../versions/2023-10-05-20-44_c2e443941930.py | 49 ++
.../database/migrations/versions/__init__.py | 0
bot_microservice/infra/logging_conf.py | 108 ++--
bot_microservice/main.py | 13 +-
bot_microservice/settings/config.py | 16 +
chat_gpt_microservice/.gitignore | 2 +-
chat_gpt_microservice/include/free_gpt.h | 5 +-
chat_gpt_microservice/src/free_gpt.cpp | 605 +++++++++++-------
chat_gpt_microservice/src/main.cpp | 26 +-
poetry.lock | 496 +++++++++++---
pyproject.toml | 16 +-
scripts/start-bot.sh | 4 +
37 files changed, 1785 insertions(+), 487 deletions(-)
create mode 100644 bot_microservice/alembic.ini
delete mode 100644 bot_microservice/api/bot/deps.py
create mode 100644 bot_microservice/api/bot/serializers.py
create mode 100644 bot_microservice/api/deps.py
create mode 100644 bot_microservice/core/bot/models/__init__.py
create mode 100644 bot_microservice/core/bot/models/chat_gpt.py
create mode 100644 bot_microservice/core/bot/repository.py
create mode 100644 bot_microservice/core/lifetime.py
create mode 100644 bot_microservice/infra/database/__init__.py
create mode 100644 bot_microservice/infra/database/base.py
create mode 100644 bot_microservice/infra/database/db_adapter.py
create mode 100644 bot_microservice/infra/database/deps.py
create mode 100644 bot_microservice/infra/database/meta.py
create mode 100644 bot_microservice/infra/database/migrations/__init__.py
create mode 100644 bot_microservice/infra/database/migrations/env.py
create mode 100644 bot_microservice/infra/database/migrations/script.py.mako
create mode 100644 bot_microservice/infra/database/migrations/versions/2023-10-05-18-28_eb78565abec7.py
create mode 100644 bot_microservice/infra/database/migrations/versions/2023-10-05-20-44_c2e443941930.py
create mode 100644 bot_microservice/infra/database/migrations/versions/__init__.py
diff --git a/README.md b/README.md
index 8d42790..60d42ca 100644
--- a/README.md
+++ b/README.md
@@ -92,6 +92,39 @@ on local start can be found at http://localhost/gpt/api/docs
prod docs https://bot.mywistr.ru/gpt/api/docs/
+
+## Create migrations
+
+Init alembic
+
+ alembic init alembic
+
+
+```bash
+cd bot_microservice
+alembic revision --autogenerate -m 'create_quads_table'
+alembic upgrade head
+```
+
+
+Create table in alembic versions
+
+```bash
+alembic --config ./alembic.ini revision -m "create account table"
+alembic --config ./alembic.ini revision --autogenerate -m 'create_quads_table'
+```
+
+
+
+Run migrations
+
+```bash
+cd ./bot_microservice # alembic root
+alembic --config ./alembic.ini upgrade head
+alembic --config ./alembic.ini downgrade 389018a3e0f0
+```
+
+
## Help article
[Следить за обновлениями этого репозитория](https://github.com/fantasy-peak/cpp-freegpt-webui)
@@ -99,10 +132,12 @@ prod docs https://bot.mywistr.ru/gpt/api/docs/
## TODO
-- [] add Database and models
-- [] add alembic migrations
+- [x] add Database and models
+- [x] add alembic migrations
- [] add models priority and their rotation
-- [] add more tests
+- [x] add update model priority endpoint
+- [] add more tests for gpt model selection
+- [] add authorisation for api
- [] reformat conftest.py file
- [x] Add sentry
- [x] Add graylog integration and availability to log to file
diff --git a/bot_microservice/alembic.ini b/bot_microservice/alembic.ini
new file mode 100644
index 0000000..70deaad
--- /dev/null
+++ b/bot_microservice/alembic.ini
@@ -0,0 +1,52 @@
+[alembic]
+script_location = infra/database/migrations
+file_template = %%(year)d-%%(month).2d-%%(day).2d-%%(hour).2d-%%(minute).2d_%%(rev)s
+prepend_sys_path = .
+output_encoding = utf-8
+
+[post_write_hooks]
+hooks = black,autoflake,isort
+
+black.type = console_scripts
+black.entrypoint = black
+
+autoflake.type = console_scripts
+autoflake.entrypoint = autoflake
+
+isort.type = console_scripts
+isort.entrypoint = isort
+
+# Logging configuration
+[loggers]
+keys = root,sqlalchemy,alembic
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+qualname =
+
+[logger_sqlalchemy]
+level = WARN
+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
diff --git a/bot_microservice/api/bot/controllers.py b/bot_microservice/api/bot/controllers.py
index d599f59..64482e8 100644
--- a/bot_microservice/api/bot/controllers.py
+++ b/bot_microservice/api/bot/controllers.py
@@ -1,7 +1,17 @@
-from fastapi import APIRouter, Request
+from fastapi import APIRouter, Body, Depends, Path
from starlette import status
-from starlette.responses import Response
+from starlette.responses import JSONResponse, Response
+from telegram import Update
+from api.bot.serializers import (
+ ChatGptModelSerializer,
+ ChatGptModelsPrioritySerializer,
+ GETChatGptModelsSerializer,
+ LightChatGptModel,
+)
+from api.deps import get_bot_queue, get_chatgpt_service, get_update_from_request
+from core.bot.app import BotQueue
+from core.bot.services import ChatGptService
from settings.config import settings
router = APIRouter()
@@ -15,5 +25,74 @@ router = APIRouter()
summary="process bot updates",
include_in_schema=False,
)
-async def process_bot_updates(request: Request) -> None:
- await request.app.state.queue.put_updates_on_queue(request)
+async def process_bot_updates(
+ tg_update: Update = Depends(get_update_from_request),
+ queue: BotQueue = Depends(get_bot_queue),
+) -> None:
+ await queue.put_updates_on_queue(tg_update)
+
+
+@router.get(
+ "/models",
+ name="bot:models_list",
+ response_class=JSONResponse,
+ response_model=list[ChatGptModelSerializer],
+ status_code=status.HTTP_200_OK,
+ summary="list of models",
+)
+async def models_list(
+ chatgpt_service: ChatGptService = Depends(get_chatgpt_service),
+) -> JSONResponse:
+ """Получить список всех моделей"""
+ models = await chatgpt_service.get_chatgpt_models()
+ return JSONResponse(
+ content=GETChatGptModelsSerializer(data=models).model_dump(), status_code=status.HTTP_200_OK # type: ignore
+ )
+
+
+@router.post(
+ "/models/{model_id}/priority",
+ name="bot:change_model_priority",
+ response_class=Response,
+ status_code=status.HTTP_202_ACCEPTED,
+ summary="change gpt model priority",
+)
+async def change_model_priority(
+ model_id: int = Path(..., gt=0, description="Id модели для обновления приореитета"),
+ chatgpt_service: ChatGptService = Depends(get_chatgpt_service),
+ gpt_model: ChatGptModelsPrioritySerializer = Body(...),
+) -> None:
+ """Изменить приоритет модели в выдаче"""
+ await chatgpt_service.change_chatgpt_model_priority(model_id=model_id, priority=gpt_model.priority)
+
+
+@router.post(
+ "/models",
+ name="bot:add_new_model",
+ response_model=ChatGptModelSerializer,
+ status_code=status.HTTP_201_CREATED,
+ summary="add new model",
+)
+async def add_new_model(
+ chatgpt_service: ChatGptService = Depends(get_chatgpt_service),
+ gpt_model: LightChatGptModel = Body(...),
+) -> JSONResponse:
+ """Добавить новую модель"""
+ model = await chatgpt_service.add_chatgpt_model(gpt_model=gpt_model.model, priority=gpt_model.priority)
+
+ return JSONResponse(content=model, status_code=status.HTTP_201_CREATED)
+
+
+@router.delete(
+ "/models/{model_id}",
+ name="bot:delete_gpt_model",
+ response_class=Response,
+ status_code=status.HTTP_204_NO_CONTENT,
+ summary="delete gpt model",
+)
+async def delete_model(
+ model_id: int = Path(..., gt=0, description="Id модели для удаления"),
+ chatgpt_service: ChatGptService = Depends(get_chatgpt_service),
+) -> None:
+ """Удалить gpt модель"""
+ await chatgpt_service.delete_chatgpt_model(model_id=model_id)
diff --git a/bot_microservice/api/bot/deps.py b/bot_microservice/api/bot/deps.py
deleted file mode 100644
index 2f35f83..0000000
--- a/bot_microservice/api/bot/deps.py
+++ /dev/null
@@ -1,13 +0,0 @@
-from fastapi import Depends
-from starlette.requests import Request
-
-from core.bot.services import ChatGptService
-from settings.config import AppSettings
-
-
-def get_settings(request: Request) -> AppSettings:
- return request.app.state.settings
-
-
-def get_chat_gpt_service(settings: AppSettings = Depends(get_settings)) -> ChatGptService:
- return ChatGptService(settings.GPT_MODEL)
diff --git a/bot_microservice/api/bot/serializers.py b/bot_microservice/api/bot/serializers.py
new file mode 100644
index 0000000..4d9ebaa
--- /dev/null
+++ b/bot_microservice/api/bot/serializers.py
@@ -0,0 +1,24 @@
+from pydantic import BaseModel, ConfigDict, Field
+
+
+class LightChatGptModel(BaseModel):
+ model: str = Field(..., title="Chat Gpt model")
+ priority: int = Field(default=0, ge=0, title="Приоритет модели")
+
+
+class ChatGptModelsPrioritySerializer(BaseModel):
+ priority: int = Field(default=0, ge=0, title="Приоритет модели")
+
+
+class ChatGptModelSerializer(BaseModel):
+ id: int = Field(..., gt=0, title="Id модели")
+ model: str = Field(..., title="Chat Gpt model")
+ priority: int = Field(..., ge=0, title="Приоритет модели")
+
+ model_config = ConfigDict(from_attributes=True)
+
+
+class GETChatGptModelsSerializer(BaseModel):
+ data: list[ChatGptModelSerializer] = Field(..., title="Список всех моделей")
+
+ model_config = ConfigDict(from_attributes=True)
diff --git a/bot_microservice/api/deps.py b/bot_microservice/api/deps.py
new file mode 100644
index 0000000..1a637a4
--- /dev/null
+++ b/bot_microservice/api/deps.py
@@ -0,0 +1,50 @@
+from fastapi import Depends
+from starlette.requests import Request
+from telegram import Update
+
+from core.bot.app import BotApplication, BotQueue
+from core.bot.repository import ChatGPTRepository
+from core.bot.services import ChatGptService, SpeechToTextService
+from infra.database.db_adapter import Database
+from settings.config import AppSettings
+
+
+def get_settings(request: Request) -> AppSettings:
+ return request.app.state.settings
+
+
+def get_bot_app(request: Request) -> BotApplication:
+ return request.app.state.bot_app
+
+
+def get_bot_queue(request: Request) -> BotQueue:
+ return request.app.state.queue
+
+
+async def get_update_from_request(request: Request, bot_app: BotApplication = Depends(get_bot_app)) -> Update | None:
+ data = await request.json()
+ return Update.de_json(data, bot_app.bot)
+
+
+def get_database(settings: AppSettings = Depends(get_settings)) -> Database:
+ return Database(settings=settings)
+
+
+def get_chat_gpt_repository(
+ db: Database = Depends(get_database), settings: AppSettings = Depends(get_settings)
+) -> ChatGPTRepository:
+ return ChatGPTRepository(settings=settings, db=db)
+
+
+def get_speech_to_text_service() -> SpeechToTextService:
+ return SpeechToTextService()
+
+
+def new_bot_queue(bot_app: BotApplication = Depends(get_bot_app)) -> BotQueue:
+ return BotQueue(bot_app=bot_app)
+
+
+def get_chatgpt_service(
+ chat_gpt_repository: ChatGPTRepository = Depends(get_chat_gpt_repository),
+) -> ChatGptService:
+ return ChatGptService(repository=chat_gpt_repository)
diff --git a/bot_microservice/api/system/controllers.py b/bot_microservice/api/system/controllers.py
index 7b0ae3a..900cee3 100644
--- a/bot_microservice/api/system/controllers.py
+++ b/bot_microservice/api/system/controllers.py
@@ -3,7 +3,7 @@ from fastapi.responses import ORJSONResponse
from starlette import status
from starlette.responses import Response
-from api.bot.deps import get_chat_gpt_service
+from api.deps import get_chatgpt_service
from api.exceptions import BaseAPIException
from constants import INVALID_GPT_REQUEST_MESSAGES
from core.bot.services import ChatGptService
@@ -33,12 +33,11 @@ async def healthcheck() -> ORJSONResponse:
)
async def gpt_healthcheck(
response: Response,
- chatgpt_service: ChatGptService = Depends(get_chat_gpt_service),
+ chatgpt_service: ChatGptService = Depends(get_chatgpt_service),
) -> Response:
- data = chatgpt_service.build_request_data("Привет!")
response.status_code = status.HTTP_200_OK
try:
- chatgpt_response = await chatgpt_service.do_request(data)
+ chatgpt_response = await chatgpt_service.request_to_chatgpt_microservice(question="Привет!")
if chatgpt_response.status_code != status.HTTP_200_OK:
response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
for message in INVALID_GPT_REQUEST_MESSAGES:
diff --git a/bot_microservice/constants.py b/bot_microservice/constants.py
index 5e8a4c2..e68c497 100644
--- a/bot_microservice/constants.py
+++ b/bot_microservice/constants.py
@@ -1,4 +1,4 @@
-from enum import StrEnum
+from enum import StrEnum, unique
AUDIO_SEGMENT_DURATION = 120 * 1000
@@ -26,3 +26,35 @@ class LogLevelEnum(StrEnum):
INFO = "info"
DEBUG = "debug"
NOTSET = ""
+
+
+@unique
+class ChatGptModelsEnum(StrEnum):
+ gpt_3_5_turbo_stream_openai = "gpt-3.5-turbo-stream-openai"
+ gpt_3_5_turbo_Aichat = "gpt-3.5-turbo-Aichat"
+ gpt_4_ChatgptAi = "gpt-4-ChatgptAi"
+ gpt_3_5_turbo_weWordle = "gpt-3.5-turbo-weWordle"
+ gpt_3_5_turbo_acytoo = "gpt-3.5-turbo-acytoo"
+ gpt_3_5_turbo_stream_DeepAi = "gpt-3.5-turbo-stream-DeepAi"
+ gpt_3_5_turbo_stream_H2o = "gpt-3.5-turbo-stream-H2o"
+ gpt_3_5_turbo_stream_yqcloud = "gpt-3.5-turbo-stream-yqcloud"
+ gpt_OpenAssistant_stream_HuggingChat = "gpt-OpenAssistant-stream-HuggingChat"
+ gpt_4_turbo_stream_you = "gpt-4-turbo-stream-you"
+ gpt_3_5_turbo_AItianhu = "gpt-3.5-turbo-AItianhu"
+ gpt_3_stream_binjie = "gpt-3-stream-binjie"
+ gpt_3_5_turbo_stream_CodeLinkAva = "gpt-3.5-turbo-stream-CodeLinkAva"
+ gpt_4_stream_ChatBase = "gpt-4-stream-ChatBase"
+ gpt_3_5_turbo_stream_aivvm = "gpt-3.5-turbo-stream-aivvm"
+ gpt_3_5_turbo_16k_stream_Ylokh = "gpt-3.5-turbo-16k-stream-Ylokh"
+ gpt_3_5_turbo_stream_Vitalentum = "gpt-3.5-turbo-stream-Vitalentum"
+ gpt_3_5_turbo_stream_GptGo = "gpt-3.5-turbo-stream-GptGo"
+ gpt_3_5_turbo_stream_AItianhuSpace = "gpt-3.5-turbo-stream-AItianhuSpace"
+ gpt_3_5_turbo_stream_Aibn = "gpt-3.5-turbo-stream-Aibn"
+ gpt_3_5_turbo_ChatgptDuo = "gpt-3.5-turbo-ChatgptDuo"
+ gpt_3_5_turbo_stream_FreeGpt = "gpt-3.5-turbo-stream-FreeGpt"
+ gpt_3_5_turbo_stream_ChatForAi = "gpt-3.5-turbo-stream-ChatForAi"
+ gpt_3_5_turbo_stream_Cromicle = "gpt-3.5-turbo-stream-Cromicle"
+
+ @classmethod
+ def values(cls) -> set[str]:
+ return set(map(str, set(ChatGptModelsEnum)))
diff --git a/bot_microservice/core/bot/app.py b/bot_microservice/core/bot/app.py
index de96c9d..a5c6c96 100644
--- a/bot_microservice/core/bot/app.py
+++ b/bot_microservice/core/bot/app.py
@@ -6,7 +6,7 @@ from functools import cached_property
from http import HTTPStatus
from typing import Any
-from fastapi import Request, Response
+from fastapi import Response
from loguru import logger
from telegram import Bot, Update
from telegram.ext import Application
@@ -68,14 +68,11 @@ class BotQueue:
bot_app: BotApplication
queue: Queue = asyncio.Queue() # type: ignore[type-arg]
- async def put_updates_on_queue(self, request: Request) -> Response:
+ async def put_updates_on_queue(self, tg_update: Update) -> Response:
"""
Listen /{URL_PREFIX}/{API_PREFIX}/{TELEGRAM_WEB_TOKEN} path and proxy post request to bot
"""
- data = await request.json()
- tg_update = Update.de_json(data=data, bot=self.bot_app.application.bot)
self.queue.put_nowait(tg_update)
-
return Response(status_code=HTTPStatus.ACCEPTED)
async def get_updates_from_queue(self) -> None:
diff --git a/bot_microservice/core/bot/commands.py b/bot_microservice/core/bot/commands.py
index 1bce780..c903681 100644
--- a/bot_microservice/core/bot/commands.py
+++ b/bot_microservice/core/bot/commands.py
@@ -67,7 +67,7 @@ async def ask_question(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
await update.message.reply_text("Пожалуйста подождите, ответ в среднем занимает 10-15 секунд")
- chat_gpt_service = ChatGptService(chat_gpt_model=settings.GPT_MODEL)
+ chat_gpt_service = ChatGptService.build()
logger.warning("question asked", user=update.message.from_user, question=update.message.text)
answer = await chat_gpt_service.request_to_chatgpt(question=update.message.text)
await update.message.reply_text(answer)
@@ -87,9 +87,9 @@ async def voice_recognize(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
logger.info("file has been saved", filename=tmpfile.name)
- speech_to_text_service = SpeechToTextService(filename=tmpfile.name)
+ speech_to_text_service = SpeechToTextService()
- speech_to_text_service.get_text_from_audio()
+ speech_to_text_service.get_text_from_audio(filename=tmpfile.name)
part = 0
while speech_to_text_service.text_parts or not speech_to_text_service.text_recognised:
diff --git a/bot_microservice/core/bot/models/__init__.py b/bot_microservice/core/bot/models/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/bot_microservice/core/bot/models/chat_gpt.py b/bot_microservice/core/bot/models/chat_gpt.py
new file mode 100644
index 0000000..24f9967
--- /dev/null
+++ b/bot_microservice/core/bot/models/chat_gpt.py
@@ -0,0 +1,12 @@
+from sqlalchemy import INTEGER, SMALLINT, VARCHAR
+from sqlalchemy.orm import Mapped, mapped_column
+
+from infra.database.base import Base
+
+__slots__ = ("ChatGpt",)
+
+
+class ChatGpt(Base):
+ id: Mapped[int] = mapped_column("id", INTEGER(), primary_key=True, autoincrement=True)
+ model: Mapped[str] = mapped_column("model", VARCHAR(length=256), nullable=False, unique=True)
+ priority: Mapped[int] = mapped_column("priority", SMALLINT(), default=0)
diff --git a/bot_microservice/core/bot/repository.py b/bot_microservice/core/bot/repository.py
new file mode 100644
index 0000000..0a83555
--- /dev/null
+++ b/bot_microservice/core/bot/repository.py
@@ -0,0 +1,106 @@
+import random
+from dataclasses import dataclass
+from typing import Any, Sequence
+from uuid import uuid4
+
+import httpx
+from httpx import AsyncClient, AsyncHTTPTransport, Response
+from loguru import logger
+from sqlalchemy import delete, desc, select, update
+from sqlalchemy.dialects.sqlite import insert
+
+from constants import CHAT_GPT_BASE_URI, INVALID_GPT_REQUEST_MESSAGES
+from core.bot.models.chat_gpt import ChatGpt
+from infra.database.db_adapter import Database
+from settings.config import AppSettings
+
+
+@dataclass
+class ChatGPTRepository:
+ settings: AppSettings
+ db: Database
+
+ async def get_chatgpt_models(self) -> Sequence[ChatGpt]:
+ query = select(ChatGpt).order_by(desc(ChatGpt.priority))
+
+ async with self.db.session() as session:
+ result = await session.execute(query)
+ return result.scalars().all()
+
+ async def change_chatgpt_model_priority(self, model_id: int, priority: int) -> None:
+ current_model = await self.get_current_chatgpt_model()
+
+ reset_priority_query = update(ChatGpt).values(priority=0).filter(ChatGpt.model == current_model)
+ set_new_priority_query = update(ChatGpt).values(priority=priority).filter(ChatGpt.model == model_id)
+
+ async with self.db.get_transaction_session() as session:
+ await session.execute(reset_priority_query)
+ await session.execute(set_new_priority_query)
+
+ async def add_chatgpt_model(self, model: str, priority: int) -> dict[str, str | int]:
+ query = (
+ insert(ChatGpt)
+ .values(
+ {ChatGpt.model: model, ChatGpt.priority: priority},
+ )
+ .prefix_with("OR IGNORE")
+ )
+ async with self.db.session() as session:
+ await session.execute(query)
+ await session.commit()
+ return {"model": model, "priority": priority}
+
+ async def delete_chatgpt_model(self, model_id: int) -> None:
+ query = delete(ChatGpt).filter_by(id=model_id)
+
+ async with self.db.session() as session:
+ await session.execute(query)
+
+ async def get_current_chatgpt_model(self) -> str:
+ query = select(ChatGpt.model).order_by(desc(ChatGpt.priority)).limit(1)
+
+ async with self.db.session() as session:
+ result = await session.execute(query)
+ return result.scalar_one()
+
+ async def ask_question(self, question: str, chat_gpt_model: str) -> str:
+ try:
+ response = await self.request_to_chatgpt_microservice(question=question, chat_gpt_model=chat_gpt_model)
+ status = response.status_code
+ for message in INVALID_GPT_REQUEST_MESSAGES:
+ if message in response.text:
+ message = f"{message}: {chat_gpt_model}"
+ logger.info(message, question=question, chat_gpt_model=chat_gpt_model)
+ return message
+ if status != httpx.codes.OK:
+ logger.info(f"got response status: {status} from chat api", response.text)
+ return "Что-то пошло не так, попробуйте еще раз или обратитесь к администратору"
+ return response.text
+ except Exception as error:
+ logger.error("error get data from chat api", error=error)
+ return "Вообще всё сломалось :("
+
+ async def request_to_chatgpt_microservice(self, question: str, chat_gpt_model: str) -> Response:
+ data = self._build_request_data(question=question, chat_gpt_model=chat_gpt_model)
+
+ transport = AsyncHTTPTransport(retries=3)
+ async with AsyncClient(base_url=self.settings.GPT_BASE_HOST, transport=transport, timeout=50) as client:
+ return await client.post(CHAT_GPT_BASE_URI, json=data, timeout=50)
+
+ @staticmethod
+ def _build_request_data(*, question: str, chat_gpt_model: str) -> dict[str, Any]:
+ return {
+ "conversation_id": str(uuid4()),
+ "action": "_ask",
+ "model": chat_gpt_model,
+ "jailbreak": "default",
+ "meta": {
+ "id": random.randint(10**18, 10**19 - 1), # noqa: S311
+ "content": {
+ "conversation": [],
+ "internet_access": False,
+ "content_type": "text",
+ "parts": [{"content": question, "role": "user"}],
+ },
+ },
+ }
diff --git a/bot_microservice/core/bot/services.py b/bot_microservice/core/bot/services.py
index 46e17ce..a520662 100644
--- a/bot_microservice/core/bot/services.py
+++ b/bot_microservice/core/bot/services.py
@@ -1,12 +1,10 @@
import os
-import random
import subprocess # noqa
from concurrent.futures.thread import ThreadPoolExecutor
-from typing import Any
-from uuid import uuid4
+from dataclasses import dataclass
+from typing import Any, Sequence
-import httpx
-from httpx import AsyncClient, AsyncHTTPTransport, Response
+from httpx import Response
from loguru import logger
from pydub import AudioSegment
from speech_recognition import (
@@ -15,33 +13,30 @@ from speech_recognition import (
UnknownValueError as SpeechRecognizerError,
)
-from constants import (
- AUDIO_SEGMENT_DURATION,
- CHAT_GPT_BASE_URI,
- INVALID_GPT_REQUEST_MESSAGES,
-)
+from constants import AUDIO_SEGMENT_DURATION
+from core.bot.models.chat_gpt import ChatGpt
+from core.bot.repository import ChatGPTRepository
+from infra.database.db_adapter import Database
from settings.config import settings
class SpeechToTextService:
- def __init__(self, filename: str) -> None:
+ def __init__(self) -> None:
self.executor = ThreadPoolExecutor()
-
- self.filename = filename
self.recognizer = Recognizer()
self.recognizer.energy_threshold = 50
self.text_parts: dict[int, str] = {}
self.text_recognised = False
- def get_text_from_audio(self) -> None:
- self.executor.submit(self.worker)
+ def get_text_from_audio(self, filename: str) -> None:
+ self.executor.submit(self.worker, filename=filename)
- def worker(self) -> Any:
- self._convert_file_to_wav()
- self._convert_audio_to_text()
+ def worker(self, filename: str) -> Any:
+ self._convert_file_to_wav(filename)
+ self._convert_audio_to_text(filename)
- def _convert_audio_to_text(self) -> None:
- wav_filename = f"{self.filename}.wav"
+ def _convert_audio_to_text(self, filename: str) -> None:
+ wav_filename = f"{filename}.wav"
speech = AudioSegment.from_wav(wav_filename)
speech_duration = len(speech)
@@ -63,18 +58,19 @@ class SpeechToTextService:
# clean temp voice message main files
try:
os.remove(wav_filename)
- os.remove(self.filename)
+ os.remove(filename)
except FileNotFoundError as error:
- logger.error("error temps files not deleted", error=error, filenames=[self.filename, self.filename])
+ logger.error("error temps files not deleted", error=error, filenames=[filename, wav_filename])
- def _convert_file_to_wav(self) -> None:
- new_filename = self.filename + ".wav"
- cmd = ["ffmpeg", "-loglevel", "quiet", "-i", self.filename, "-vn", new_filename]
+ @staticmethod
+ def _convert_file_to_wav(filename: str) -> None:
+ new_filename = filename + ".wav"
+ cmd = ["ffmpeg", "-loglevel", "quiet", "-i", filename, "-vn", new_filename]
try:
subprocess.run(args=cmd) # noqa: S603
logger.info("file has been converted to wav", filename=new_filename)
except Exception as error:
- logger.error("cant convert voice", error=error, filename=self.filename)
+ logger.error("cant convert voice", error=error, filename=filename)
def _recognize_by_google(self, filename: str, sound_segment: AudioSegment) -> str:
tmp_filename = f"{filename}_tmp_part"
@@ -91,48 +87,36 @@ class SpeechToTextService:
raise error
+@dataclass
class ChatGptService:
- def __init__(self, chat_gpt_model: str) -> None:
- self.chat_gpt_model = chat_gpt_model
+ repository: ChatGPTRepository
+
+ async def get_chatgpt_models(self) -> Sequence[ChatGpt]:
+ return await self.repository.get_chatgpt_models()
async def request_to_chatgpt(self, question: str | None) -> str:
question = question or "Привет!"
- chat_gpt_request = self.build_request_data(question)
- try:
- response = await self.do_request(chat_gpt_request)
- status = response.status_code
- for message in INVALID_GPT_REQUEST_MESSAGES:
- if message in response.text:
- message = f"{message}: {settings.GPT_MODEL}"
- logger.info(message, data=chat_gpt_request)
- return message
- if status != httpx.codes.OK:
- logger.info(f"got response status: {status} from chat api", data=chat_gpt_request)
- return "Что-то пошло не так, попробуйте еще раз или обратитесь к администратору"
- return response.text
- except Exception as error:
- logger.error("error get data from chat api", error=error)
- return "Вообще всё сломалось :("
+ chat_gpt_model = await self.get_current_chatgpt_model()
+ return await self.repository.ask_question(question=question, chat_gpt_model=chat_gpt_model)
- @staticmethod
- async def do_request(data: dict[str, Any]) -> Response:
- transport = AsyncHTTPTransport(retries=3)
- async with AsyncClient(base_url=settings.GPT_BASE_HOST, transport=transport, timeout=50) as client:
- return await client.post(CHAT_GPT_BASE_URI, json=data, timeout=50)
+ async def request_to_chatgpt_microservice(self, question: str) -> Response:
+ chat_gpt_model = await self.get_current_chatgpt_model()
+ return await self.repository.request_to_chatgpt_microservice(question=question, chat_gpt_model=chat_gpt_model)
- def build_request_data(self, question: str) -> dict[str, Any]:
- return {
- "conversation_id": str(uuid4()),
- "action": "_ask",
- "model": self.chat_gpt_model,
- "jailbreak": "default",
- "meta": {
- "id": random.randint(10**18, 10**19 - 1), # noqa: S311
- "content": {
- "conversation": [],
- "internet_access": False,
- "content_type": "text",
- "parts": [{"content": question, "role": "user"}],
- },
- },
- }
+ async def get_current_chatgpt_model(self) -> str:
+ return await self.repository.get_current_chatgpt_model()
+
+ async def change_chatgpt_model_priority(self, model_id: int, priority: int) -> None:
+ return await self.repository.change_chatgpt_model_priority(model_id=model_id, priority=priority)
+
+ async def add_chatgpt_model(self, gpt_model: str, priority: int) -> dict[str, str | int]:
+ return await self.repository.add_chatgpt_model(model=gpt_model, priority=priority)
+
+ async def delete_chatgpt_model(self, model_id: int) -> None:
+ return await self.repository.delete_chatgpt_model(model_id=model_id)
+
+ @classmethod
+ def build(cls) -> "ChatGptService":
+ db = Database(settings=settings)
+ repository = ChatGPTRepository(settings=settings, db=db)
+ return ChatGptService(repository=repository)
diff --git a/bot_microservice/core/lifetime.py b/bot_microservice/core/lifetime.py
new file mode 100644
index 0000000..8be5a6a
--- /dev/null
+++ b/bot_microservice/core/lifetime.py
@@ -0,0 +1,73 @@
+from asyncio import current_task
+from typing import Awaitable, Callable
+
+from fastapi import FastAPI
+from sqlalchemy.ext.asyncio import (
+ AsyncSession,
+ async_scoped_session,
+ async_sessionmaker,
+ create_async_engine,
+)
+
+from settings.config import AppSettings
+
+
+def startup(app: FastAPI, settings: AppSettings) -> Callable[[], Awaitable[None]]:
+ """
+ Actions to run on application startup.
+
+ This function use fastAPI app to store data,
+ such as db_engine.
+
+ :param app: the fastAPI application.
+ :param settings: app settings
+ :return: function that actually performs actions.
+
+ """
+
+ async def _startup() -> None:
+ _setup_db(app, settings)
+
+ return _startup
+
+
+def shutdown(app: FastAPI) -> Callable[[], Awaitable[None]]:
+ """
+ Actions to run on application's shutdown.
+
+ :param app: fastAPI application.
+ :return: function that actually performs actions.
+
+ """
+
+ async def _shutdown() -> None:
+ await app.state.db_engine.dispose()
+
+ return _shutdown
+
+
+def _setup_db(app: FastAPI, settings: AppSettings) -> None:
+ """
+ Create connection to the database.
+
+ This function creates SQLAlchemy engine instance,
+ session_factory for creating sessions
+ and stores them in the application's state property.
+
+ :param app: fastAPI application.
+ """
+ engine = create_async_engine(
+ str(settings.db_url),
+ echo=settings.DB_ECHO,
+ execution_options={"isolation_level": "AUTOCOMMIT"},
+ )
+ session_factory = async_scoped_session(
+ async_sessionmaker(
+ engine,
+ expire_on_commit=False,
+ class_=AsyncSession,
+ ),
+ scopefunc=current_task,
+ )
+ app.state.db_engine = engine
+ app.state.db_session_factory = session_factory
diff --git a/bot_microservice/core/utils.py b/bot_microservice/core/utils.py
index 5f9243a..548642d 100644
--- a/bot_microservice/core/utils.py
+++ b/bot_microservice/core/utils.py
@@ -1,5 +1,6 @@
from datetime import datetime, timedelta
from functools import lru_cache, wraps
+from inspect import cleandoc
from typing import Any
@@ -22,3 +23,9 @@ def timed_cache(**timedelta_kwargs: Any) -> Any:
return _wrapped
return _wrapper
+
+
+def clean_doc(cls: Any) -> str | None:
+ if cls.__doc__ is None:
+ return None
+ return cleandoc(cls.__doc__)
diff --git a/bot_microservice/infra/database/__init__.py b/bot_microservice/infra/database/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/bot_microservice/infra/database/base.py b/bot_microservice/infra/database/base.py
new file mode 100644
index 0000000..227e946
--- /dev/null
+++ b/bot_microservice/infra/database/base.py
@@ -0,0 +1,34 @@
+from sqlalchemy import Table, inspect
+from sqlalchemy.orm import as_declarative, declared_attr
+
+from infra.database.meta import meta
+
+
+@as_declarative(metadata=meta)
+class Base:
+ """
+ Base for all models.
+
+ It has some type definitions to
+ enhance autocompletion.
+ """
+
+ # Generate __tablename__ automatically
+ @declared_attr
+ def __tablename__(self) -> str:
+ return self.__name__.lower()
+
+ __table__: Table
+
+ @classmethod
+ def get_real_column_name(cls, attr_name: str) -> str:
+ return getattr(inspect(cls).c, attr_name).name # type: ignore
+
+ def __str__(self) -> str:
+ return self.__repr__()
+
+ def __repr__(self) -> str:
+ try:
+ return f"{self.__class__.__name__}(id={self.id})" # type: ignore[attr-defined]
+ except AttributeError:
+ return super().__repr__()
diff --git a/bot_microservice/infra/database/db_adapter.py b/bot_microservice/infra/database/db_adapter.py
new file mode 100644
index 0000000..1d64181
--- /dev/null
+++ b/bot_microservice/infra/database/db_adapter.py
@@ -0,0 +1,102 @@
+import os
+import pkgutil
+from asyncio import current_task
+from contextlib import asynccontextmanager
+from pathlib import Path
+from typing import AsyncGenerator
+
+from loguru import logger
+from sqlalchemy.ext.asyncio import (
+ AsyncEngine,
+ AsyncSession,
+ async_scoped_session,
+ async_sessionmaker,
+ create_async_engine,
+)
+
+from settings.config import AppSettings
+
+
+class Database:
+ def __init__(self, settings: AppSettings) -> None:
+ self.db_connect_url = settings.db_url
+ self.echo_logs = settings.DB_ECHO
+ self.db_file = settings.DB_FILE
+ self._engine: AsyncEngine = create_async_engine(
+ str(settings.db_url),
+ echo=settings.DB_ECHO,
+ execution_options={"isolation_level": "AUTOCOMMIT"},
+ )
+ self._async_session_factory = async_scoped_session(
+ async_sessionmaker(
+ autoflush=False,
+ class_=AsyncSession,
+ expire_on_commit=False,
+ bind=self._engine,
+ ),
+ scopefunc=current_task,
+ )
+
+ @asynccontextmanager
+ async def session(self) -> AsyncGenerator[AsyncSession, None]:
+ session: AsyncSession = self._async_session_factory()
+
+ async with session:
+ try:
+ yield session
+ except Exception:
+ await session.rollback()
+ raise
+
+ @asynccontextmanager
+ async def get_transaction_session(self) -> AsyncGenerator[AsyncSession, None]:
+ async with self._async_session_factory() as session, session.begin():
+ try:
+ yield session
+ except Exception as error:
+ await session.rollback()
+ raise error
+
+ async def create_database(self) -> None:
+ """
+ Create a test database.
+
+ :param engine: Async engine for database creation
+ :param db_path: path to sqlite file
+
+ """
+ if not self.db_file.exists():
+ from infra.database.meta import meta
+
+ load_all_models()
+ try:
+ async with self._engine.begin() as connection:
+ await connection.run_sync(meta.create_all)
+
+ logger.info("all migrations are applied")
+ except Exception as err:
+ logger.error("Cant run migrations", err=err)
+
+ async def drop_database(self) -> None:
+ """
+ Drop current database.
+
+ :param path: Delete sqlite database file
+
+ """
+ if self.db_file.exists():
+ os.remove(self.db_file)
+
+
+def load_all_models() -> None:
+ """Load all models from this folder."""
+ package_dir = Path(__file__).resolve().parent.parent
+ package_dir = package_dir.joinpath("core")
+ modules = pkgutil.walk_packages(path=[str(package_dir)], prefix="core.")
+ models_packages = [module for module in modules if module.ispkg and "models" in module.name]
+ for module in models_packages:
+ model_pkgs = pkgutil.walk_packages(
+ path=[os.path.join(str(module.module_finder.path), "models")], prefix=f"{module.name}." # type: ignore
+ )
+ for model_pkg in model_pkgs:
+ __import__(model_pkg.name)
diff --git a/bot_microservice/infra/database/deps.py b/bot_microservice/infra/database/deps.py
new file mode 100644
index 0000000..eecf86b
--- /dev/null
+++ b/bot_microservice/infra/database/deps.py
@@ -0,0 +1,20 @@
+from typing import AsyncGenerator
+
+from sqlalchemy.ext.asyncio import AsyncSession
+from starlette.requests import Request
+
+
+async def get_db_session(request: Request) -> AsyncGenerator[AsyncSession, None]:
+ """
+ Create and get database session.
+
+ :param request: current request.
+ :yield: database session.
+ """
+ session: AsyncSession = request.app.state.db_session_factory()
+
+ try:
+ yield session
+ finally:
+ await session.commit()
+ await session.close()
diff --git a/bot_microservice/infra/database/meta.py b/bot_microservice/infra/database/meta.py
new file mode 100644
index 0000000..5d24aa1
--- /dev/null
+++ b/bot_microservice/infra/database/meta.py
@@ -0,0 +1,3 @@
+from sqlalchemy import MetaData
+
+meta = MetaData()
diff --git a/bot_microservice/infra/database/migrations/__init__.py b/bot_microservice/infra/database/migrations/__init__.py
new file mode 100644
index 0000000..6dccb95
--- /dev/null
+++ b/bot_microservice/infra/database/migrations/__init__.py
@@ -0,0 +1 @@
+"""Alembic migraions."""
diff --git a/bot_microservice/infra/database/migrations/env.py b/bot_microservice/infra/database/migrations/env.py
new file mode 100644
index 0000000..b115747
--- /dev/null
+++ b/bot_microservice/infra/database/migrations/env.py
@@ -0,0 +1,81 @@
+import asyncio
+from logging.config import fileConfig
+
+from alembic import context
+from sqlalchemy.ext.asyncio.engine import create_async_engine
+from sqlalchemy.future import Connection
+
+from infra.database.db_adapter import load_all_models
+from infra.database.meta import meta
+from settings.config import settings
+
+# this is the Alembic Config object, which provides
+# access to the values within the .ini file in use.
+config = context.config
+
+# for 'autogenerate' support from myapp import mymodel
+load_all_models()
+
+# 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
+target_metadata = meta
+
+
+async 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.
+
+ """
+ context.configure(
+ url=str(settings.db_url),
+ target_metadata=target_metadata,
+ literal_binds=True,
+ dialect_opts={"paramstyle": "named"},
+ )
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+def do_run_migrations(connection: Connection) -> None:
+ """
+ Run actual sync migrations.
+
+ :param connection: connection to the database.
+
+ """
+ context.configure(connection=connection, target_metadata=target_metadata)
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+async 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 = create_async_engine(str(settings.db_url))
+
+ async with connectable.connect() as connection:
+ await connection.run_sync(do_run_migrations)
+
+
+if context.is_offline_mode():
+ asyncio.run(run_migrations_offline())
+else:
+ asyncio.run(run_migrations_online())
diff --git a/bot_microservice/infra/database/migrations/script.py.mako b/bot_microservice/infra/database/migrations/script.py.mako
new file mode 100644
index 0000000..55df286
--- /dev/null
+++ b/bot_microservice/infra/database/migrations/script.py.mako
@@ -0,0 +1,24 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision = ${repr(up_revision)}
+down_revision = ${repr(down_revision)}
+branch_labels = ${repr(branch_labels)}
+depends_on = ${repr(depends_on)}
+
+
+def upgrade() -> None:
+ ${upgrades if upgrades else "pass"}
+
+
+def downgrade() -> None:
+ ${downgrades if downgrades else "pass"}
diff --git a/bot_microservice/infra/database/migrations/versions/2023-10-05-18-28_eb78565abec7.py b/bot_microservice/infra/database/migrations/versions/2023-10-05-18-28_eb78565abec7.py
new file mode 100644
index 0000000..a977a8b
--- /dev/null
+++ b/bot_microservice/infra/database/migrations/versions/2023-10-05-18-28_eb78565abec7.py
@@ -0,0 +1,34 @@
+"""initial commit
+
+Revision ID: eb78565abec7
+Revises:
+Create Date: 2023-10-05 18:28:30.915361
+
+"""
+import sqlalchemy as sa
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision = "eb78565abec7"
+down_revision = None
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table(
+ "chatgpt",
+ sa.Column("id", sa.INTEGER(), autoincrement=True, nullable=False),
+ sa.Column("model", sa.VARCHAR(length=256), nullable=False),
+ sa.Column("priority", sa.SMALLINT(), nullable=False),
+ sa.PrimaryKeyConstraint("id"),
+ sa.UniqueConstraint("model"),
+ )
+ # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_table("chatgpt")
+ # ### end Alembic commands ###
diff --git a/bot_microservice/infra/database/migrations/versions/2023-10-05-20-44_c2e443941930.py b/bot_microservice/infra/database/migrations/versions/2023-10-05-20-44_c2e443941930.py
new file mode 100644
index 0000000..6af811b
--- /dev/null
+++ b/bot_microservice/infra/database/migrations/versions/2023-10-05-20-44_c2e443941930.py
@@ -0,0 +1,49 @@
+"""create chat gpt models
+
+Revision ID: c2e443941930
+Revises: eb78565abec7
+Create Date: 2025-10-05 20:44:05.414977
+
+"""
+
+from sqlalchemy import create_engine, select
+from sqlalchemy.orm import sessionmaker
+
+from constants import ChatGptModelsEnum
+from core.bot.models.chat_gpt import ChatGpt
+from settings.config import settings
+
+# revision identifiers, used by Alembic.
+revision = "c2e443941930"
+down_revision = "eb78565abec7"
+branch_labels: str | None = None
+depends_on: str | None = None
+
+engine = create_engine(str(settings.db_url), echo=settings.DB_ECHO)
+session_factory = sessionmaker(engine)
+
+
+def upgrade() -> None:
+ with session_factory() as session:
+ query = select(ChatGpt)
+ results = session.execute(query)
+ models = results.scalars().all()
+
+ if models:
+ return None
+ models = []
+ for model in ChatGptModelsEnum:
+ priority = 0 if model != "gpt-3.5-turbo-stream-FreeGpt" else 1
+ fields = {"model": model, "priority": priority}
+ models.append(ChatGpt(**fields))
+ session.add_all(models)
+ session.commit()
+
+
+def downgrade() -> None:
+ with session_factory() as session:
+ session.execute(f"""TRUNCATE TABLE {ChatGpt.__tablename__}""")
+ session.commit()
+
+
+engine.dispose()
diff --git a/bot_microservice/infra/database/migrations/versions/__init__.py b/bot_microservice/infra/database/migrations/versions/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/bot_microservice/infra/logging_conf.py b/bot_microservice/infra/logging_conf.py
index 789fe56..190f97d 100644
--- a/bot_microservice/infra/logging_conf.py
+++ b/bot_microservice/infra/logging_conf.py
@@ -17,6 +17,56 @@ else:
Record = dict[str, Any]
+class Formatter:
+ @staticmethod
+ def json_formatter(record: Record) -> str:
+ # Обрезаем `\n` в конце логов, т.к. в json формате переносы не нужны
+ return Formatter.scrap_sensitive_info(record.get("message", "").strip())
+
+ @staticmethod
+ def sentry_formatter(record: Record) -> str:
+ if message := record.get("message", ""):
+ record["message"] = Formatter.scrap_sensitive_info(message)
+ return "{name}:{function} {message}"
+
+ @staticmethod
+ def text_formatter(record: Record) -> str:
+ # WARNING !!!
+ # Функция должна возвращать строку, которая содержит только шаблоны для форматирования.
+ # Если в строку прокидывать значения из record (или еще откуда-либо),
+ # то loguru может принять их за f-строки и попытается обработать, что приведет к ошибке.
+ # Например, если нужно достать какое-то значение из поля extra, вместо того чтобы прокидывать его в строку
+ # формата, нужно прокидывать подстроку вида {extra[тут_ключ]}
+
+ if message := record.get("message", ""):
+ record["message"] = Formatter.scrap_sensitive_info(message)
+
+ # Стандартный формат loguru. Задается через env LOGURU_FORMAT
+ format_ = (
+ "{time:YYYY-MM-DD HH:mm:ss.SSS} | "
+ "{level: <8} | "
+ "{name}:{function}:{line} - "
+ "{message}"
+ )
+
+ # Добавляем мета параметры по типу user_id, art_id, которые передаются через logger.bind(...)
+ extra = record["extra"]
+ if extra:
+ formatted = ", ".join(f"{key}" + "={extra[" + str(key) + "]}" for key, value in extra.items())
+ format_ += f" - {formatted}"
+
+ format_ += "\n"
+
+ if record["exception"] is not None:
+ format_ += "{exception}\n"
+
+ return format_
+
+ @staticmethod
+ def scrap_sensitive_info(message: str) -> str:
+ return message.replace(settings.TELEGRAM_API_TOKEN, "TELEGRAM_API_TOKEN".center(24, "*"))
+
+
class InterceptHandler(logging.Handler):
def emit(self, record: logging.LogRecord) -> None:
# Get corresponding Loguru level if it exists
@@ -31,13 +81,9 @@ class InterceptHandler(logging.Handler):
frame = cast(FrameType, frame.f_back)
depth += 1
- logger.opt(depth=depth, exception=record.exc_info).log(level, self._scrap_sensitive_info(record))
-
- @staticmethod
- def _scrap_sensitive_info(record: logging.LogRecord) -> str:
- message = record.getMessage()
- message.replace(settings.TELEGRAM_API_TOKEN, "TELEGRAM_API_TOKEN".center(24, "*"))
- return message
+ logger.opt(depth=depth, exception=record.exc_info).log(
+ level, Formatter.scrap_sensitive_info(record.getMessage())
+ )
def configure_logging(
@@ -45,7 +91,7 @@ def configure_logging(
) -> None:
intercept_handler = InterceptHandler()
- formatter = _json_formatter if enable_json_logs else _text_formatter
+ formatter = Formatter.json_formatter if enable_json_logs else Formatter.text_formatter
base_config_handlers = [intercept_handler]
@@ -64,10 +110,10 @@ def configure_logging(
base_config_handlers.append(graylog_handler)
loguru_handlers.append({**base_loguru_handler, "sink": graylog_handler})
if log_to_file:
- file_path = os.path.join(DIR_LOGS, log_to_file)
+ file_path = DIR_LOGS / log_to_file
if not os.path.exists(log_to_file):
- with open(file_path, 'w') as f:
- f.write('')
+ with open(file_path, "w") as f:
+ f.write("")
loguru_handlers.append({**base_loguru_handler, "sink": file_path})
logging.basicConfig(handlers=base_config_handlers, level=level.name)
@@ -78,42 +124,4 @@ def configure_logging(
# https://forum.sentry.io/t/changing-issue-title-when-logging-with-traceback/446
if enable_sentry_logs:
handler = EventHandler(level=logging.WARNING)
- logger.add(handler, diagnose=True, level=logging.WARNING, format=_sentry_formatter)
-
-
-def _json_formatter(record: Record) -> str:
- # Обрезаем `\n` в конце логов, т.к. в json формате переносы не нужны
- return record.get("message", "").strip()
-
-
-def _sentry_formatter(record: Record) -> str:
- return "{name}:{function} {message}"
-
-
-def _text_formatter(record: Record) -> str:
- # WARNING !!!
- # Функция должна возвращать строку, которая содержит только шаблоны для форматирования.
- # Если в строку прокидывать значения из record (или еще откуда-либо),
- # то loguru может принять их за f-строки и попытается обработать, что приведет к ошибке.
- # Например, если нужно достать какое-то значение из поля extra, вместо того чтобы прокидывать его в строку формата,
- # нужно прокидывать подстроку вида {extra[тут_ключ]}
-
- # Стандартный формат loguru. Задается через env LOGURU_FORMAT
- format_ = (
- "{time:YYYY-MM-DD HH:mm:ss.SSS} | "
- "{level: <8} | "
- "{name}:{function}:{line} - {message}"
- )
-
- # Добавляем мета параметры по типу user_id, art_id, которые передаются через logger.bind(...)
- extra = record["extra"]
- if extra:
- formatted = ", ".join(f"{key}" + "={extra[" + str(key) + "]}" for key, value in extra.items())
- format_ += f" - {formatted}"
-
- format_ += "\n"
-
- if record["exception"] is not None:
- format_ += "{exception}\n"
-
- return format_
+ logger.add(handler, diagnose=True, level=logging.WARNING, format=Formatter.sentry_formatter)
diff --git a/bot_microservice/main.py b/bot_microservice/main.py
index 3e2e113..981fcce 100644
--- a/bot_microservice/main.py
+++ b/bot_microservice/main.py
@@ -8,6 +8,7 @@ from fastapi.responses import UJSONResponse
from constants import LogLevelEnum
from core.bot.app import BotApplication, BotQueue
from core.bot.handlers import bot_event_handlers
+from core.lifetime import shutdown, startup
from infra.logging_conf import configure_logging
from routers import api_router
from settings.config import AppSettings, get_settings
@@ -26,8 +27,12 @@ class Application:
)
self.app.state.settings = settings
self.app.state.queue = BotQueue(bot_app=bot_app)
+ self.app.state.bot_app = bot_app
self.bot_app = bot_app
+ self.app.on_event("startup")(startup(self.app, settings))
+ self.app.on_event("shutdown")(shutdown(self.app))
+
self.app.include_router(api_router)
self.configure_hooks()
configure_logging(
@@ -51,18 +56,18 @@ class Application:
def configure_hooks(self) -> None:
if self.bot_app.start_with_webhook:
- self.app.add_event_handler("startup", self._on_start_up)
+ self.app.add_event_handler("startup", self._bot_start_up)
else:
self.app.add_event_handler("startup", self.bot_app.polling)
- self.app.add_event_handler("shutdown", self._on_shutdown)
+ self.app.add_event_handler("shutdown", self._bot_shutdown)
- async def _on_start_up(self) -> None:
+ async def _bot_start_up(self) -> None:
await self.bot_app.set_webhook()
loop = asyncio.get_event_loop()
loop.create_task(self.app.state.queue.get_updates_from_queue())
- async def _on_shutdown(self) -> None:
+ async def _bot_shutdown(self) -> None:
await asyncio.gather(self.bot_app.delete_webhook(), self.bot_app.shutdown())
diff --git a/bot_microservice/settings/config.py b/bot_microservice/settings/config.py
index 820f869..4065af4 100644
--- a/bot_microservice/settings/config.py
+++ b/bot_microservice/settings/config.py
@@ -6,6 +6,7 @@ from typing import Any
from dotenv import load_dotenv
from pydantic import model_validator
from pydantic_settings import BaseSettings
+from yarl import URL
from constants import API_PREFIX
@@ -52,6 +53,9 @@ class AppSettings(SentrySettings, BaseSettings):
DOMAIN: str = "https://localhost"
URL_PREFIX: str = ""
+ DB_FILE: Path = SHARED_DIR / "chat_gpt.db"
+ DB_ECHO: bool = False
+
# ==== gpt settings ====
GPT_MODEL: str = "gpt-3.5-turbo-stream-DeepAi"
GPT_BASE_HOST: str = "http://chat_service:8858"
@@ -91,6 +95,18 @@ class AppSettings(SentrySettings, BaseSettings):
def bot_webhook_url(self) -> str:
return "/".join([self.api_prefix, self.token_part])
+ @cached_property
+ def db_url(self) -> URL:
+ """
+ Assemble database URL from settings.
+
+ :return: database URL.
+ """
+ return URL.build(
+ scheme="sqlite+aiosqlite",
+ path=f"///{self.DB_FILE}",
+ )
+
class Config:
case_sensitive = True
diff --git a/chat_gpt_microservice/.gitignore b/chat_gpt_microservice/.gitignore
index e5279d7..0247e19 100644
--- a/chat_gpt_microservice/.gitignore
+++ b/chat_gpt_microservice/.gitignore
@@ -154,4 +154,4 @@ cython_debug/
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
-#.idea/
+.idea/
diff --git a/chat_gpt_microservice/include/free_gpt.h b/chat_gpt_microservice/include/free_gpt.h
index b2b6309..3c55fe4 100644
--- a/chat_gpt_microservice/include/free_gpt.h
+++ b/chat_gpt_microservice/include/free_gpt.h
@@ -23,7 +23,6 @@ public:
boost::asio::awaitable deepAi(std::shared_ptr, nlohmann::json);
boost::asio::awaitable aiChat(std::shared_ptr, nlohmann::json);
boost::asio::awaitable chatGptAi(std::shared_ptr, nlohmann::json);
- boost::asio::awaitable weWordle(std::shared_ptr, nlohmann::json);
boost::asio::awaitable acytoo(std::shared_ptr, nlohmann::json);
boost::asio::awaitable openAi(std::shared_ptr, nlohmann::json);
boost::asio::awaitable h2o(std::shared_ptr, nlohmann::json);
@@ -31,7 +30,6 @@ public:
boost::asio::awaitable huggingChat(std::shared_ptr, nlohmann::json);
boost::asio::awaitable you(std::shared_ptr, nlohmann::json);
boost::asio::awaitable binjie(std::shared_ptr, nlohmann::json);
- boost::asio::awaitable codeLinkAva(std::shared_ptr, nlohmann::json);
boost::asio::awaitable chatBase(std::shared_ptr, nlohmann::json);
boost::asio::awaitable aivvm(std::shared_ptr, nlohmann::json);
boost::asio::awaitable ylokh(std::shared_ptr, nlohmann::json);
@@ -39,6 +37,9 @@ public:
boost::asio::awaitable gptGo(std::shared_ptr, nlohmann::json);
boost::asio::awaitable aibn(std::shared_ptr, nlohmann::json);
boost::asio::awaitable chatGptDuo(std::shared_ptr, nlohmann::json);
+ boost::asio::awaitable chatForAi(std::shared_ptr, nlohmann::json);
+ boost::asio::awaitable freeGpt(std::shared_ptr, nlohmann::json);
+ boost::asio::awaitable cromicle(std::shared_ptr, nlohmann::json);
private:
boost::asio::awaitable, std::string>>
diff --git a/chat_gpt_microservice/src/free_gpt.cpp b/chat_gpt_microservice/src/free_gpt.cpp
index 2b97ddb..ea18a48 100644
--- a/chat_gpt_microservice/src/free_gpt.cpp
+++ b/chat_gpt_microservice/src/free_gpt.cpp
@@ -641,11 +641,15 @@ FreeGpt::createHttpClient(boost::asio::ssl::context& ctx, std::string_view host,
}
boost::asio::awaitable FreeGpt::deepAi(std::shared_ptr ch, nlohmann::json json) {
- ScopeExit auto_exit{[&] { ch->close(); }};
+ co_await boost::asio::post(boost::asio::bind_executor(*m_thread_pool_ptr, boost::asio::use_awaitable));
+
boost::system::error_code err{};
+ ScopeExit _exit{[=] { boost::asio::post(ch->get_executor(), [=] { ch->close(); }); }};
+ auto prompt = json.at("meta").at("content").at("parts").at(0).at("content").get();
std::string user_agent{
- R"(Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36)"};
+ R"(Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36)"};
+
std::random_device rd;
std::mt19937 mt(rd());
std::uniform_int_distribution dist(0, 100000000);
@@ -654,19 +658,39 @@ boost::asio::awaitable FreeGpt::deepAi(std::shared_ptr ch, nlohma
auto api_key = std::format("tryit-{}-{}", part1, part2);
constexpr char CRLF[] = "\r\n";
- constexpr char MULTI_PART_BOUNDARY[] = "9bc627aea4f77e150e6057f78036e73f";
- constexpr std::string_view host{"api.deepai.org"};
- constexpr std::string_view port{"443"};
+ static std::string MULTI_PART_BOUNDARY = "9bc627aea4f77e150e6057f78036e73f";
- boost::beast::http::request req{boost::beast::http::verb::post,
- "/make_me_a_sandwich", 11};
- req.set(boost::beast::http::field::host, host);
- req.set(boost::beast::http::field::user_agent, user_agent);
- req.set("Api-Key", api_key);
- req.set(boost::beast::http::field::content_type,
- std::format("multipart/form-data; boundary={}", MULTI_PART_BOUNDARY));
+ CURLcode res;
+ CURL* curl = curl_easy_init();
+ if (!curl) {
+ auto error_info = std::format("curl_easy_init() failed:{}", curl_easy_strerror(res));
+ co_await boost::asio::post(boost::asio::bind_executor(ch->get_executor(), boost::asio::use_awaitable));
+ ch->try_send(err, error_info);
+ co_return;
+ }
+ curl_easy_setopt(curl, CURLOPT_URL, "https://api.deepai.org/hacking_is_a_crime");
+
+ if (!m_cfg.http_proxy.empty())
+ curl_easy_setopt(curl, CURLOPT_PROXY, m_cfg.http_proxy.c_str());
+
+ struct Input {
+ std::shared_ptr ch;
+ std::string recv;
+ };
+ Input input{ch};
+ auto action_cb = [](void* contents, size_t size, size_t nmemb, void* userp) -> size_t {
+ boost::system::error_code err{};
+ auto input_ptr = static_cast(userp);
+ std::string data{(char*)contents, size * nmemb};
+ auto& [ch, recv] = *input_ptr;
+ boost::asio::post(ch->get_executor(), [=] { ch->try_send(err, data); });
+ return size * nmemb;
+ };
+ size_t (*action_fn)(void* contents, size_t size, size_t nmemb, void* userp) = action_cb;
+ curlEasySetopt(curl);
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, action_fn);
+ curl_easy_setopt(curl, CURLOPT_WRITEDATA, &input);
- auto prompt = json.at("meta").at("content").at("parts").at(0).at("content").get();
nlohmann::json request_json{{{"role", "user"}, {"content", std::move(prompt)}}};
std::ostringstream payload;
@@ -674,30 +698,37 @@ boost::asio::awaitable FreeGpt::deepAi(std::shared_ptr ch, nlohma
<< CRLF << "chat" << CRLF << "--" << MULTI_PART_BOUNDARY << CRLF
<< R"(Content-Disposition: form-data; name="chatHistory")" << CRLF << CRLF << request_json.dump() << CRLF
<< "--" << MULTI_PART_BOUNDARY << "--" << CRLF;
-
SPDLOG_INFO("{}", payload.str());
- req.body() = payload.str();
- req.prepare_payload();
+ auto str = payload.str();
+ curl_easy_setopt(curl, CURLOPT_POSTFIELDS, str.c_str());
- int recreate_num{0};
-create_client:
- boost::asio::ssl::context ctx(boost::asio::ssl::context::tls);
- ctx.set_verify_mode(boost::asio::ssl::verify_none);
- auto client = co_await createHttpClient(ctx, host, port);
- if (!client.has_value()) {
- SPDLOG_ERROR("createHttpClient: {}", client.error());
- co_await ch->async_send(err, client.error(), use_nothrow_awaitable);
+ struct curl_slist* headers = nullptr;
+ auto content_type_str = std::format("Content-Type: multipart/form-data; boundary={}", MULTI_PART_BOUNDARY);
+ SPDLOG_INFO("content_type_str: {}", content_type_str);
+ headers = curl_slist_append(headers, content_type_str.c_str());
+ auto api_key_str = std::format("api-key: {}", api_key);
+ headers = curl_slist_append(headers, api_key_str.c_str());
+ curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
+
+ ScopeExit auto_exit{[=] {
+ curl_slist_free_all(headers);
+ curl_easy_cleanup(curl);
+ }};
+
+ res = curl_easy_perform(curl);
+
+ if (res != CURLE_OK) {
+ co_await boost::asio::post(boost::asio::bind_executor(ch->get_executor(), boost::asio::use_awaitable));
+ auto error_info = std::format("curl_easy_perform() failed:{}", curl_easy_strerror(res));
+ ch->try_send(err, error_info);
co_return;
}
- auto& stream_ = client.value();
-
- auto ret = co_await sendRequestRecvChunk(ch, stream_, req, 200, [&ch](std::string recv_str) {
- boost::system::error_code ec{};
- ch->try_send(ec, recv_str);
- });
- if (ret == Status::Close && recreate_num == 0) {
- recreate_num++;
- goto create_client;
+ int32_t response_code;
+ curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);
+ if (response_code != 200) {
+ co_await boost::asio::post(boost::asio::bind_executor(ch->get_executor(), boost::asio::use_awaitable));
+ ch->try_send(err, std::format("deepai http code:{}", response_code));
+ co_return;
}
co_return;
}
@@ -1007,116 +1038,6 @@ create_client:
co_return;
}
-boost::asio::awaitable FreeGpt::weWordle(std::shared_ptr ch, nlohmann::json json) {
- ScopeExit auto_exit{[&] { ch->close(); }};
- boost::system::error_code err{};
-
- auto prompt = json.at("meta").at("content").at("parts").at(0).at("content").get();
-
- auto random = [](int len) {
- static std::string chars{"abcdefghijklmnopqrstuvwxyz0123456789"};
- static std::string letter{"abcdefghijklmnopqrstuvwxyz"};
- std::random_device rd;
- std::mt19937 gen(rd());
- std::uniform_int_distribution<> dis(0, 1000000);
- std::string random_string;
- random_string += chars[dis(gen) % letter.length()];
- len = len - 1;
- for (int i = 0; i < len; i++)
- random_string += chars[dis(gen) % chars.length()];
- return random_string;
- };
- auto user_id = random(16);
- auto app_id = random(31);
- auto now = std::chrono::time_point_cast(std::chrono::system_clock::now());
- auto request_date = std::format("{:%Y-%m-%dT%H:%M:%S.000Z}", now);
-
- constexpr std::string_view host = "wewordle.org";
- constexpr std::string_view port = "443";
-
- boost::beast::http::request req{boost::beast::http::verb::post,
- "/gptapi/v1/android/turbo", 11};
- req.set(boost::beast::http::field::host, host);
- req.set("pragma", "no-cache");
- req.set("accept", "*/*");
- req.set(
- boost::beast::http::field::user_agent,
- R"(Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36)");
- req.set(boost::beast::http::field::content_type, "application/json");
- req.set("Accept-Encoding", "gzip, deflate");
-
- constexpr std::string_view json_str = R"({
- "user":"j1b892x978flimoa",
- "messages":[
- {
- "role":"user",
- "content":"user: hello\nassistant:"
- }
- ],
- "subscriber":{
- "originalPurchaseDate":null,
- "originalApplicationVersion":null,
- "allPurchaseDatesMillis":{},
- "entitlements":{
- "active":{},
- "all":{}
- },
- "allPurchaseDates":{},
- "allExpirationDatesMillis":{},
- "allExpirationDates":{},
- "originalAppUserId":"$RCAnonymousID:z6xyxaasvt841d5zttw7q2iisb023tf",
- "latestExpirationDate":null,
- "requestDate":"2023-08-03T00:29:53.000Z",
- "latestExpirationDateMillis":null,
- "nonSubscriptionTransactions":[],
- "originalPurchaseDateMillis":null,
- "managementURL":null,
- "allPurchasedProductIdentifiers":[],
- "firstSeen":"2023-08-03T00:29:53.000Z",
- "activeSubscriptions":[]
- }
- })";
-
- nlohmann::json request = nlohmann::json::parse(json_str, nullptr, false);
-
- request["user"] = user_id;
- request["subscriber"]["originalAppUserId"] = std::format("$RCAnonymousID:{}", app_id);
- request["subscriber"]["firstSeen"] = request_date;
- request["subscriber"]["requestDate"] = request_date;
- request["messages"][0]["content"] = std::format("user: {}\nassistant:", prompt);
-
- SPDLOG_INFO("{}", request.dump(2));
-
- req.body() = request.dump();
- req.prepare_payload();
-
- auto ret = co_await sendRequestRecvResponse(req, host, port, std::bind_front(&FreeGpt::createHttpClient, *this));
- if (!ret.has_value()) {
- co_await ch->async_send(err, ret.error(), use_nothrow_awaitable);
- co_return;
- }
- auto& [res, ctx, stream_] = ret.value();
- if (boost::beast::http::status::ok != res.result()) {
- SPDLOG_ERROR("http status code: {}", res.result_int());
- co_await ch->async_send(err, res.reason(), use_nothrow_awaitable);
- co_return;
- }
-
- nlohmann::json rsp = nlohmann::json::parse(res.body(), nullptr, false);
- if (rsp.is_discarded()) {
- SPDLOG_ERROR("json parse error");
- co_await ch->async_send(err, std::format("json parse error: {}", res.body()), use_nothrow_awaitable);
- co_return;
- }
- if (!rsp.contains("message")) {
- SPDLOG_ERROR("not contains message: {}", rsp.dump());
- co_await ch->async_send(err, std::format("not contains message : {}", rsp.dump()), use_nothrow_awaitable);
- co_return;
- }
- co_await ch->async_send(err, rsp["message"].value("content", rsp.dump()), use_nothrow_awaitable);
- co_return;
-}
-
boost::asio::awaitable FreeGpt::acytoo(std::shared_ptr ch, nlohmann::json json) {
boost::system::error_code err{};
ScopeExit auto_exit{[&] { ch->close(); }};
@@ -1606,7 +1527,7 @@ boost::asio::awaitable FreeGpt::huggingChat(std::shared_ptr ch, n
req_init_conversation.set(boost::beast::http::field::user_agent, user_agent);
req_init_conversation.set("Accept", "*/*");
req_init_conversation.set("Content-Type", "application/json");
- req_init_conversation.body() = R"({"model": "OpenAssistant/oasst-sft-6-llama-30b-xor"})";
+ req_init_conversation.body() = R"({"model": "meta-llama/Llama-2-70b-chat-hf"})";
req_init_conversation.prepare_payload();
auto [ec, count] = co_await boost::beast::http::async_write(stream_, req_init_conversation, use_nothrow_awaitable);
@@ -1902,87 +1823,6 @@ boost::asio::awaitable FreeGpt::binjie(std::shared_ptr ch, nlohma
co_return;
}
-boost::asio::awaitable FreeGpt::codeLinkAva(std::shared_ptr ch, nlohmann::json json) {
- boost::system::error_code err{};
- ScopeExit auto_exit{[&] { ch->close(); }};
-
- auto prompt = json.at("meta").at("content").at("parts").at(0).at("content").get();
-
- constexpr std::string_view host = "ava-alpha-api.codelink.io";
- constexpr std::string_view port = "443";
-
- constexpr std::string_view user_agent{
- R"(Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36)"};
-
- boost::asio::ssl::context ctx(boost::asio::ssl::context::tls);
- ctx.set_verify_mode(boost::asio::ssl::verify_none);
-
- auto client = co_await createHttpClient(ctx, host, port);
- if (!client.has_value()) {
- SPDLOG_ERROR("createHttpClient: {}", client.error());
- co_await ch->async_send(err, client.error(), use_nothrow_awaitable);
- co_return;
- }
- auto& stream_ = client.value();
-
- boost::beast::http::request req{boost::beast::http::verb::post, "/api/chat", 11};
- req.set(boost::beast::http::field::host, host);
- req.set(boost::beast::http::field::user_agent, user_agent);
- req.set("Accept", "*/*");
- req.set("accept-language", "en,fr-FR;q=0.9,fr;q=0.8,es-ES;q=0.7,es;q=0.6,en-US;q=0.5,am;q=0.4,de;q=0.3");
- req.set("origin", "https://ava-ai-ef611.web.app");
- req.set("referer", "https://ava-ai-ef611.web.app/");
- req.set(boost::beast::http::field::content_type, "application/json");
- req.set("sec-fetch-dest", "empty");
- req.set("sec-fetch-mode", "cors");
- req.set("sec-fetch-site", "same-origin");
-
- constexpr std::string_view json_str = R"({
- "messages": [
- {
- "role": "user",
- "content": "hello"
- }
- ],
- "stream": true,
- "temperature": 0.6
- })";
- nlohmann::json request = nlohmann::json::parse(json_str, nullptr, false);
-
- request["messages"][0]["content"] = prompt;
- SPDLOG_INFO("{}", request.dump(2));
-
- req.body() = request.dump();
- req.prepare_payload();
-
- std::string recv;
- auto result = co_await sendRequestRecvChunk(ch, stream_, req, 200, [&ch, &recv](std::string chunk_str) {
- recv.append(chunk_str);
- while (true) {
- auto position = recv.find("\n");
- if (position == std::string::npos)
- break;
- auto msg = recv.substr(0, position + 1);
- recv.erase(0, position + 1);
- msg.pop_back();
- if (msg.empty() || !msg.contains("content"))
- continue;
- auto fields = splitString(msg, "data:");
- boost::system::error_code err{};
- nlohmann::json line_json = nlohmann::json::parse(fields.back(), nullptr, false);
- if (line_json.is_discarded()) {
- SPDLOG_ERROR("json parse error: [{}]", fields.back());
- ch->try_send(err, std::format("json parse error: [{}]", fields.back()));
- continue;
- }
- auto str = line_json["choices"][0]["delta"]["content"].get();
- if (!str.empty())
- ch->try_send(err, str);
- }
- });
- co_return;
-}
-
boost::asio::awaitable FreeGpt::chatBase(std::shared_ptr ch, nlohmann::json json) {
boost::system::error_code err{};
ScopeExit auto_exit{[&] { ch->close(); }};
@@ -2703,3 +2543,318 @@ boost::asio::awaitable FreeGpt::chatGptDuo(std::shared_ptr ch, nl
}
co_return;
}
+
+boost::asio::awaitable FreeGpt::chatForAi(std::shared_ptr ch, nlohmann::json json) {
+ co_await boost::asio::post(boost::asio::bind_executor(*m_thread_pool_ptr, boost::asio::use_awaitable));
+
+ boost::system::error_code err{};
+ ScopeExit _exit{[=] { boost::asio::post(ch->get_executor(), [=] { ch->close(); }); }};
+ auto prompt = json.at("meta").at("content").at("parts").at(0).at("content").get();
+
+ CURLcode res;
+ CURL* curl = curl_easy_init();
+ if (!curl) {
+ auto error_info = std::format("curl_easy_init() failed:{}", curl_easy_strerror(res));
+ co_await boost::asio::post(boost::asio::bind_executor(ch->get_executor(), boost::asio::use_awaitable));
+ ch->try_send(err, error_info);
+ co_return;
+ }
+ curl_easy_setopt(curl, CURLOPT_URL, "https://chatforai.com/api/handle/provider-openai");
+ if (!m_cfg.http_proxy.empty())
+ curl_easy_setopt(curl, CURLOPT_PROXY, m_cfg.http_proxy.c_str());
+
+ struct Input {
+ std::shared_ptr ch;
+ std::string recv;
+ };
+ Input input{ch};
+ auto action_cb = [](void* contents, size_t size, size_t nmemb, void* userp) -> size_t {
+ boost::system::error_code err{};
+ auto input_ptr = static_cast(userp);
+ std::string data{(char*)contents, size * nmemb};
+ auto& [ch, recv] = *input_ptr;
+ boost::asio::post(ch->get_executor(), [=] { ch->try_send(err, data); });
+ return size * nmemb;
+ };
+ size_t (*action_fn)(void* contents, size_t size, size_t nmemb, void* userp) = action_cb;
+ curlEasySetopt(curl);
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, action_fn);
+ curl_easy_setopt(curl, CURLOPT_WRITEDATA, &input);
+
+ auto generate_signature = [](int timestamp, const std::string& conversation_id, const std::string& message) {
+ std::stringstream ss;
+ ss << timestamp << ":" << conversation_id << ":" << message << ":6B46K4pt";
+ std::string data = ss.str();
+
+ unsigned char digest[SHA256_DIGEST_LENGTH];
+ SHA256(reinterpret_cast(data.c_str()), data.length(), digest);
+
+ std::stringstream sha_stream;
+ for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
+ sha_stream << std::setfill('0') << std::setw(2) << std::hex << static_cast(digest[i]);
+ }
+ return sha_stream.str();
+ };
+ uint64_t timestamp =
+ std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count();
+ auto onversation_id = std::format("id_{}", timestamp);
+ std::string signature = generate_signature(timestamp, onversation_id, prompt);
+
+ constexpr std::string_view request_str{R"({
+ "conversationId": "id_1696338587",
+ "conversationType": "chat_continuous",
+ "botId": "chat_continuous",
+ "globalSettings": {
+ "baseUrl": "https://api.openai.com",
+ "model": "gpt-3.5-turbo",
+ "messageHistorySize": 5,
+ "temperature": 0.7,
+ "top_p": 1,
+ "stream": true
+ },
+ "botSettings": {},
+ "prompt": "hello",
+ "messages": [{
+ "role": "user",
+ "content": "hello"
+ }],
+ "sign": "1505ec882d72d5f3175a74ac84d665b1b904e6671b2e7334268f540975929a26",
+ "timestamp": 1696338587
+ })"};
+ nlohmann::json request = nlohmann::json::parse(request_str, nullptr, false);
+
+ request["sign"] = signature;
+ request["conversationId"] = onversation_id;
+ request["messages"] = getConversationJson(json);
+ request["timestamp"] = timestamp;
+
+ auto str = request.dump();
+ SPDLOG_INFO("request : [{}]", str);
+
+ curl_easy_setopt(curl, CURLOPT_POSTFIELDS, str.c_str());
+
+ struct curl_slist* headers = nullptr;
+ headers = curl_slist_append(headers, "Content-Type: application/json");
+ curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
+
+ ScopeExit auto_exit{[=] {
+ curl_slist_free_all(headers);
+ curl_easy_cleanup(curl);
+ }};
+
+ res = curl_easy_perform(curl);
+
+ if (res != CURLE_OK) {
+ co_await boost::asio::post(boost::asio::bind_executor(ch->get_executor(), boost::asio::use_awaitable));
+ auto error_info = std::format("curl_easy_perform() failed:{}", curl_easy_strerror(res));
+ ch->try_send(err, error_info);
+ co_return;
+ }
+ int32_t response_code;
+ curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);
+ if (response_code != 200) {
+ co_await boost::asio::post(boost::asio::bind_executor(ch->get_executor(), boost::asio::use_awaitable));
+ ch->try_send(err, std::format("you http code:{}", response_code));
+ co_return;
+ }
+ co_return;
+}
+
+boost::asio::awaitable FreeGpt::freeGpt(std::shared_ptr ch, nlohmann::json json) {
+ co_await boost::asio::post(boost::asio::bind_executor(*m_thread_pool_ptr, boost::asio::use_awaitable));
+
+ boost::system::error_code err{};
+ ScopeExit _exit{[=] { boost::asio::post(ch->get_executor(), [=] { ch->close(); }); }};
+ auto prompt = json.at("meta").at("content").at("parts").at(0).at("content").get();
+
+ CURLcode res;
+ CURL* curl = curl_easy_init();
+ if (!curl) {
+ auto error_info = std::format("curl_easy_init() failed:{}", curl_easy_strerror(res));
+ co_await boost::asio::post(boost::asio::bind_executor(ch->get_executor(), boost::asio::use_awaitable));
+ ch->try_send(err, error_info);
+ co_return;
+ }
+ curl_easy_setopt(curl, CURLOPT_URL, "https://k.aifree.site/api/generate");
+ if (!m_cfg.http_proxy.empty())
+ curl_easy_setopt(curl, CURLOPT_PROXY, m_cfg.http_proxy.c_str());
+
+ struct Input {
+ std::shared_ptr ch;
+ std::string recv;
+ };
+ Input input{ch};
+ auto action_cb = [](void* contents, size_t size, size_t nmemb, void* userp) -> size_t {
+ boost::system::error_code err{};
+ auto input_ptr = static_cast(userp);
+ std::string data{(char*)contents, size * nmemb};
+ auto& [ch, recv] = *input_ptr;
+ boost::asio::post(ch->get_executor(), [=] { ch->try_send(err, data); });
+ return size * nmemb;
+ };
+ size_t (*action_fn)(void* contents, size_t size, size_t nmemb, void* userp) = action_cb;
+ curlEasySetopt(curl);
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, action_fn);
+ curl_easy_setopt(curl, CURLOPT_WRITEDATA, &input);
+
+ auto generate_signature = [](int timestamp, const std::string& message, const std::string& secret = "") {
+ std::stringstream ss;
+ ss << timestamp << ":" << message << ":" << secret;
+ std::string data = ss.str();
+
+ unsigned char digest[SHA256_DIGEST_LENGTH];
+ SHA256(reinterpret_cast(data.c_str()), data.length(), digest);
+
+ std::stringstream sha_stream;
+ for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
+ sha_stream << std::setfill('0') << std::setw(2) << std::hex << static_cast(digest[i]);
+ }
+ return sha_stream.str();
+ };
+ uint64_t timestamp =
+ std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count();
+ std::string signature = generate_signature(timestamp, prompt);
+
+ constexpr std::string_view request_str{R"({
+ "messages":[
+ {
+ "role":"user",
+ "content":"hello"
+ }
+ ],
+ "pass":null,
+ "sign":"7c2700b5813053ff8000cb9fb1ebdadbfcf62882829da59e4474bee466de7c89",
+ "time":1695716667
+ })"};
+ nlohmann::json request = nlohmann::json::parse(request_str, nullptr, false);
+
+ request["sign"] = signature;
+ request["time"] = timestamp;
+ request["messages"] = getConversationJson(json);
+
+ auto str = request.dump();
+ SPDLOG_INFO("request : [{}]", str);
+
+ curl_easy_setopt(curl, CURLOPT_POSTFIELDS, str.c_str());
+
+ struct curl_slist* headers = nullptr;
+ headers = curl_slist_append(headers, "Content-Type: application/json");
+ curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
+
+ ScopeExit auto_exit{[=] {
+ curl_slist_free_all(headers);
+ curl_easy_cleanup(curl);
+ }};
+
+ res = curl_easy_perform(curl);
+
+ if (res != CURLE_OK) {
+ co_await boost::asio::post(boost::asio::bind_executor(ch->get_executor(), boost::asio::use_awaitable));
+ auto error_info = std::format("curl_easy_perform() failed:{}", curl_easy_strerror(res));
+ ch->try_send(err, error_info);
+ co_return;
+ }
+ int32_t response_code;
+ curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);
+ if (response_code != 200) {
+ co_await boost::asio::post(boost::asio::bind_executor(ch->get_executor(), boost::asio::use_awaitable));
+ ch->try_send(err, std::format("you http code:{}", response_code));
+ co_return;
+ }
+ co_return;
+}
+
+boost::asio::awaitable FreeGpt::cromicle(std::shared_ptr ch, nlohmann::json json) {
+ co_await boost::asio::post(boost::asio::bind_executor(*m_thread_pool_ptr, boost::asio::use_awaitable));
+
+ boost::system::error_code err{};
+ ScopeExit _exit{[=] { boost::asio::post(ch->get_executor(), [=] { ch->close(); }); }};
+ auto prompt = json.at("meta").at("content").at("parts").at(0).at("content").get();
+
+ CURLcode res;
+ CURL* curl = curl_easy_init();
+ if (!curl) {
+ auto error_info = std::format("curl_easy_init() failed:{}", curl_easy_strerror(res));
+ co_await boost::asio::post(boost::asio::bind_executor(ch->get_executor(), boost::asio::use_awaitable));
+ ch->try_send(err, error_info);
+ co_return;
+ }
+ curl_easy_setopt(curl, CURLOPT_URL, "https://cromicle.top/chat");
+ if (!m_cfg.http_proxy.empty())
+ curl_easy_setopt(curl, CURLOPT_PROXY, m_cfg.http_proxy.c_str());
+
+ struct Input {
+ std::shared_ptr ch;
+ std::string recv;
+ };
+ Input input{ch};
+ auto action_cb = [](void* contents, size_t size, size_t nmemb, void* userp) -> size_t {
+ boost::system::error_code err{};
+ auto input_ptr = static_cast(userp);
+ std::string data{(char*)contents, size * nmemb};
+ auto& [ch, recv] = *input_ptr;
+ boost::asio::post(ch->get_executor(), [=] { ch->try_send(err, data); });
+ return size * nmemb;
+ };
+ size_t (*action_fn)(void* contents, size_t size, size_t nmemb, void* userp) = action_cb;
+ curlEasySetopt(curl);
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, action_fn);
+ curl_easy_setopt(curl, CURLOPT_WRITEDATA, &input);
+
+ auto generate_signature = [](const std::string& message) {
+ std::stringstream ss;
+ ss << "asdap" << message;
+ std::string data = ss.str();
+
+ unsigned char digest[SHA256_DIGEST_LENGTH];
+ SHA256(reinterpret_cast(data.c_str()), data.length(), digest);
+
+ std::stringstream sha_stream;
+ for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
+ sha_stream << std::setfill('0') << std::setw(2) << std::hex << static_cast(digest[i]);
+ }
+ return sha_stream.str();
+ };
+ std::string signature = generate_signature(prompt);
+
+ constexpr std::string_view request_str{R"({
+ "message": "hello",
+ "hash": "dda6ea4e1dc215f198084018b1df20cfeafe9fbdfe31d8a350d6917509158d8a",
+ "token": "asdap"
+ })"};
+ nlohmann::json request = nlohmann::json::parse(request_str, nullptr, false);
+
+ request["hash"] = signature;
+ request["message"] = prompt;
+
+ auto str = request.dump();
+ SPDLOG_INFO("request : [{}]", str);
+
+ curl_easy_setopt(curl, CURLOPT_POSTFIELDS, str.c_str());
+
+ struct curl_slist* headers = nullptr;
+ headers = curl_slist_append(headers, "Content-Type: application/json");
+ curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
+
+ ScopeExit auto_exit{[=] {
+ curl_slist_free_all(headers);
+ curl_easy_cleanup(curl);
+ }};
+
+ res = curl_easy_perform(curl);
+
+ if (res != CURLE_OK) {
+ co_await boost::asio::post(boost::asio::bind_executor(ch->get_executor(), boost::asio::use_awaitable));
+ auto error_info = std::format("curl_easy_perform() failed:{}", curl_easy_strerror(res));
+ ch->try_send(err, error_info);
+ co_return;
+ }
+ int32_t response_code;
+ curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);
+ if (response_code != 200) {
+ co_await boost::asio::post(boost::asio::bind_executor(ch->get_executor(), boost::asio::use_awaitable));
+ ch->try_send(err, std::format("you http code:{}", response_code));
+ co_return;
+ }
+ co_return;
+}
diff --git a/chat_gpt_microservice/src/main.cpp b/chat_gpt_microservice/src/main.cpp
index 8072c10..ee959ad 100644
--- a/chat_gpt_microservice/src/main.cpp
+++ b/chat_gpt_microservice/src/main.cpp
@@ -92,6 +92,18 @@ boost::asio::awaitable sendHttpResponse(auto& stream, auto& request, auto
co_return;
}
+void setContentType(auto& res, const std::string& file) {
+ SPDLOG_INFO("file: {}", file);
+ if (file.ends_with("js")) {
+ res.set(boost::beast::http::field::content_type, "text/javascript");
+ } else if (file.ends_with("css")) {
+ res.set(boost::beast::http::field::content_type, "text/css");
+ } else if (file.ends_with("png")) {
+ res.set(boost::beast::http::field::content_type, "image/png");
+ } else
+ SPDLOG_ERROR("invalid file type: {}", file);
+}
+
boost::asio::awaitable startSession(boost::asio::ip::tcp::socket sock, Config& cfg,
boost::asio::io_context& context) {
boost::beast::tcp_stream stream{std::move(sock)};
@@ -172,7 +184,7 @@ boost::asio::awaitable startSession(boost::asio::ip::tcp::socket sock, Con
boost::beast::http::response res{boost::beast::http::status::ok,
request.version()};
res.set(boost::beast::http::field::server, BOOST_BEAST_VERSION_STRING);
- res.set(boost::beast::http::field::content_type, "text/javascript");
+ setContentType(res, file);
res.keep_alive(request.keep_alive());
res.body() = std::move(chat_js_content);
res.prepare_payload();
@@ -191,8 +203,7 @@ boost::asio::awaitable startSession(boost::asio::ip::tcp::socket sock, Con
std::piecewise_construct, std::make_tuple(std::move(body)),
std::make_tuple(boost::beast::http::status::ok, request.version())};
res.set(boost::beast::http::field::server, BOOST_BEAST_VERSION_STRING);
- res.set(boost::beast::http::field::content_type,
- req_path.contains("css") ? "text/css" : "text/javascript");
+ setContentType(res, file);
res.content_length(size);
res.keep_alive(request.keep_alive());
boost::beast::http::message_generator rsp = std::move(res);
@@ -324,24 +335,25 @@ int main(int argc, char** argv) {
ADD_METHOD("gpt-3.5-turbo-stream-openai", FreeGpt::openAi);
ADD_METHOD("gpt-3.5-turbo-Aichat", FreeGpt::aiChat);
ADD_METHOD("gpt-4-ChatgptAi", FreeGpt::chatGptAi);
- // ADD_METHOD("gpt-3.5-turbo-weWordle", FreeGpt::weWordle);
ADD_METHOD("gpt-3.5-turbo-acytoo", FreeGpt::acytoo);
ADD_METHOD("gpt-3.5-turbo-stream-DeepAi", FreeGpt::deepAi);
ADD_METHOD("gpt-3.5-turbo-stream-H2o", FreeGpt::h2o);
ADD_METHOD("gpt-3.5-turbo-stream-yqcloud", FreeGpt::yqcloud);
ADD_METHOD("gpt-OpenAssistant-stream-HuggingChat", FreeGpt::huggingChat)
ADD_METHOD("gpt-4-turbo-stream-you", FreeGpt::you);
- ADD_METHOD("gpt-3.5-turbo-AItianhu", FreeGpt::aiTianhu);
+ // ADD_METHOD("gpt-3.5-turbo-AItianhu", FreeGpt::aiTianhu);
ADD_METHOD("gpt-3-stream-binjie", FreeGpt::binjie);
- // ADD_METHOD("gpt-3.5-turbo-stream-CodeLinkAva", FreeGpt::codeLinkAva);
ADD_METHOD("gpt-4-stream-ChatBase", FreeGpt::chatBase);
ADD_METHOD("gpt-3.5-turbo-stream-aivvm", FreeGpt::aivvm);
ADD_METHOD("gpt-3.5-turbo-16k-stream-Ylokh", FreeGpt::ylokh);
ADD_METHOD("gpt-3.5-turbo-stream-Vitalentum", FreeGpt::vitalentum);
ADD_METHOD("gpt-3.5-turbo-stream-GptGo", FreeGpt::gptGo);
- ADD_METHOD("gpt-3.5-turbo-stream-AItianhuSpace", FreeGpt::aiTianhuSpace);
+ // ADD_METHOD("gpt-3.5-turbo-stream-AItianhuSpace", FreeGpt::aiTianhuSpace);
ADD_METHOD("gpt-3.5-turbo-stream-Aibn", FreeGpt::aibn);
ADD_METHOD("gpt-3.5-turbo-ChatgptDuo", FreeGpt::chatGptDuo);
+ ADD_METHOD("gpt-3.5-turbo-stream-ChatForAi", FreeGpt::chatForAi);
+ ADD_METHOD("gpt-3.5-turbo-stream-FreeGpt", FreeGpt::freeGpt);
+ ADD_METHOD("gpt-3.5-turbo-stream-Cromicle", FreeGpt::cromicle);
IoContextPool pool{cfg.work_thread_num};
pool.start();
diff --git a/poetry.lock b/poetry.lock
index e553189..ca34199 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -12,14 +12,48 @@ files = [
]
[[package]]
-name = "annotated-types"
-version = "0.5.0"
-description = "Reusable constraint types to use with typing.Annotated"
+name = "aiosqlite"
+version = "0.19.0"
+description = "asyncio bridge to the standard sqlite3 module"
optional = false
python-versions = ">=3.7"
files = [
- {file = "annotated_types-0.5.0-py3-none-any.whl", hash = "sha256:58da39888f92c276ad970249761ebea80ba544b77acddaa1a4d6cf78287d45fd"},
- {file = "annotated_types-0.5.0.tar.gz", hash = "sha256:47cdc3490d9ac1506ce92c7aaa76c579dc3509ff11e098fc867e5130ab7be802"},
+ {file = "aiosqlite-0.19.0-py3-none-any.whl", hash = "sha256:edba222e03453e094a3ce605db1b970c4b3376264e56f32e2a4959f948d66a96"},
+ {file = "aiosqlite-0.19.0.tar.gz", hash = "sha256:95ee77b91c8d2808bd08a59fbebf66270e9090c3d92ffbf260dc0db0b979577d"},
+]
+
+[package.extras]
+dev = ["aiounittest (==1.4.1)", "attribution (==1.6.2)", "black (==23.3.0)", "coverage[toml] (==7.2.3)", "flake8 (==5.0.4)", "flake8-bugbear (==23.3.12)", "flit (==3.7.1)", "mypy (==1.2.0)", "ufmt (==2.1.0)", "usort (==1.0.6)"]
+docs = ["sphinx (==6.1.3)", "sphinx-mdinclude (==0.5.3)"]
+
+[[package]]
+name = "alembic"
+version = "1.12.0"
+description = "A database migration tool for SQLAlchemy."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "alembic-1.12.0-py3-none-any.whl", hash = "sha256:03226222f1cf943deee6c85d9464261a6c710cd19b4fe867a3ad1f25afda610f"},
+ {file = "alembic-1.12.0.tar.gz", hash = "sha256:8e7645c32e4f200675e69f0745415335eb59a3663f5feb487abfa0b30c45888b"},
+]
+
+[package.dependencies]
+Mako = "*"
+SQLAlchemy = ">=1.3.0"
+typing-extensions = ">=4"
+
+[package.extras]
+tz = ["python-dateutil"]
+
+[[package]]
+name = "annotated-types"
+version = "0.6.0"
+description = "Reusable constraint types to use with typing.Annotated"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"},
+ {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"},
]
[[package]]
@@ -1031,75 +1065,77 @@ docs = ["sphinx (>=2.1.2,<3.0.0)", "sphinx-autodoc-typehints (>=1.6.0,<2.0.0)",
[[package]]
name = "greenlet"
-version = "2.0.2"
+version = "3.0.0"
description = "Lightweight in-process concurrent programming"
optional = false
-python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*"
+python-versions = ">=3.7"
files = [
- {file = "greenlet-2.0.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:bdfea8c661e80d3c1c99ad7c3ff74e6e87184895bbaca6ee8cc61209f8b9b85d"},
- {file = "greenlet-2.0.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9d14b83fab60d5e8abe587d51c75b252bcc21683f24699ada8fb275d7712f5a9"},
- {file = "greenlet-2.0.2-cp27-cp27m-win32.whl", hash = "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74"},
- {file = "greenlet-2.0.2-cp27-cp27m-win_amd64.whl", hash = "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343"},
- {file = "greenlet-2.0.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae"},
- {file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df"},
- {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088"},
- {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb"},
- {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d75209eed723105f9596807495d58d10b3470fa6732dd6756595e89925ce2470"},
- {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a51c9751078733d88e013587b108f1b7a1fb106d402fb390740f002b6f6551a"},
- {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91"},
- {file = "greenlet-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645"},
- {file = "greenlet-2.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c"},
- {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca"},
- {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0"},
- {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2"},
- {file = "greenlet-2.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:eff4eb9b7eb3e4d0cae3d28c283dc16d9bed6b193c2e1ace3ed86ce48ea8df19"},
- {file = "greenlet-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5454276c07d27a740c5892f4907c86327b632127dd9abec42ee62e12427ff7e3"},
- {file = "greenlet-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:7cafd1208fdbe93b67c7086876f061f660cfddc44f404279c1585bbf3cdc64c5"},
- {file = "greenlet-2.0.2-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:910841381caba4f744a44bf81bfd573c94e10b3045ee00de0cbf436fe50673a6"},
- {file = "greenlet-2.0.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:18a7f18b82b52ee85322d7a7874e676f34ab319b9f8cce5de06067384aa8ff43"},
- {file = "greenlet-2.0.2-cp35-cp35m-win32.whl", hash = "sha256:03a8f4f3430c3b3ff8d10a2a86028c660355ab637cee9333d63d66b56f09d52a"},
- {file = "greenlet-2.0.2-cp35-cp35m-win_amd64.whl", hash = "sha256:4b58adb399c4d61d912c4c331984d60eb66565175cdf4a34792cd9600f21b394"},
- {file = "greenlet-2.0.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:703f18f3fda276b9a916f0934d2fb6d989bf0b4fb5a64825260eb9bfd52d78f0"},
- {file = "greenlet-2.0.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:32e5b64b148966d9cccc2c8d35a671409e45f195864560829f395a54226408d3"},
- {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dd11f291565a81d71dab10b7033395b7a3a5456e637cf997a6f33ebdf06f8db"},
- {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0f72c9ddb8cd28532185f54cc1453f2c16fb417a08b53a855c4e6a418edd099"},
- {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd021c754b162c0fb55ad5d6b9d960db667faad0fa2ff25bb6e1301b0b6e6a75"},
- {file = "greenlet-2.0.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:3c9b12575734155d0c09d6c3e10dbd81665d5c18e1a7c6597df72fd05990c8cf"},
- {file = "greenlet-2.0.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b9ec052b06a0524f0e35bd8790686a1da006bd911dd1ef7d50b77bfbad74e292"},
- {file = "greenlet-2.0.2-cp36-cp36m-win32.whl", hash = "sha256:dbfcfc0218093a19c252ca8eb9aee3d29cfdcb586df21049b9d777fd32c14fd9"},
- {file = "greenlet-2.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:9f35ec95538f50292f6d8f2c9c9f8a3c6540bbfec21c9e5b4b751e0a7c20864f"},
- {file = "greenlet-2.0.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:d5508f0b173e6aa47273bdc0a0b5ba055b59662ba7c7ee5119528f466585526b"},
- {file = "greenlet-2.0.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:f82d4d717d8ef19188687aa32b8363e96062911e63ba22a0cff7802a8e58e5f1"},
- {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9c59a2120b55788e800d82dfa99b9e156ff8f2227f07c5e3012a45a399620b7"},
- {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2780572ec463d44c1d3ae850239508dbeb9fed38e294c68d19a24d925d9223ca"},
- {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:937e9020b514ceedb9c830c55d5c9872abc90f4b5862f89c0887033ae33c6f73"},
- {file = "greenlet-2.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:36abbf031e1c0f79dd5d596bfaf8e921c41df2bdf54ee1eed921ce1f52999a86"},
- {file = "greenlet-2.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:18e98fb3de7dba1c0a852731c3070cf022d14f0d68b4c87a19cc1016f3bb8b33"},
- {file = "greenlet-2.0.2-cp37-cp37m-win32.whl", hash = "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7"},
- {file = "greenlet-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3"},
- {file = "greenlet-2.0.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30"},
- {file = "greenlet-2.0.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b"},
- {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526"},
- {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b"},
- {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acd2162a36d3de67ee896c43effcd5ee3de247eb00354db411feb025aa319857"},
- {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0bf60faf0bc2468089bdc5edd10555bab6e85152191df713e2ab1fcc86382b5a"},
- {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a"},
- {file = "greenlet-2.0.2-cp38-cp38-win32.whl", hash = "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249"},
- {file = "greenlet-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40"},
- {file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8"},
- {file = "greenlet-2.0.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6"},
- {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df"},
- {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be4ed120b52ae4d974aa40215fcdfde9194d63541c7ded40ee12eb4dda57b76b"},
- {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94c817e84245513926588caf1152e3b559ff794d505555211ca041f032abbb6b"},
- {file = "greenlet-2.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1a819eef4b0e0b96bb0d98d797bef17dc1b4a10e8d7446be32d1da33e095dbb8"},
- {file = "greenlet-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7efde645ca1cc441d6dc4b48c0f7101e8d86b54c8530141b09fd31cef5149ec9"},
- {file = "greenlet-2.0.2-cp39-cp39-win32.whl", hash = "sha256:ea9872c80c132f4663822dd2a08d404073a5a9b5ba6155bea72fb2a79d1093b5"},
- {file = "greenlet-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:db1a39669102a1d8d12b57de2bb7e2ec9066a6f2b3da35ae511ff93b01b5d564"},
- {file = "greenlet-2.0.2.tar.gz", hash = "sha256:e7c8dc13af7db097bed64a051d2dd49e9f0af495c26995c00a9ee842690d34c0"},
+ {file = "greenlet-3.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e09dea87cc91aea5500262993cbd484b41edf8af74f976719dd83fe724644cd6"},
+ {file = "greenlet-3.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f47932c434a3c8d3c86d865443fadc1fbf574e9b11d6650b656e602b1797908a"},
+ {file = "greenlet-3.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bdfaeecf8cc705d35d8e6de324bf58427d7eafb55f67050d8f28053a3d57118c"},
+ {file = "greenlet-3.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a68d670c8f89ff65c82b936275369e532772eebc027c3be68c6b87ad05ca695"},
+ {file = "greenlet-3.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38ad562a104cd41e9d4644f46ea37167b93190c6d5e4048fcc4b80d34ecb278f"},
+ {file = "greenlet-3.0.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02a807b2a58d5cdebb07050efe3d7deaf915468d112dfcf5e426d0564aa3aa4a"},
+ {file = "greenlet-3.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b1660a15a446206c8545edc292ab5c48b91ff732f91b3d3b30d9a915d5ec4779"},
+ {file = "greenlet-3.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:813720bd57e193391dfe26f4871186cf460848b83df7e23e6bef698a7624b4c9"},
+ {file = "greenlet-3.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:aa15a2ec737cb609ed48902b45c5e4ff6044feb5dcdfcf6fa8482379190330d7"},
+ {file = "greenlet-3.0.0-cp310-universal2-macosx_11_0_x86_64.whl", hash = "sha256:7709fd7bb02b31908dc8fd35bfd0a29fc24681d5cc9ac1d64ad07f8d2b7db62f"},
+ {file = "greenlet-3.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:211ef8d174601b80e01436f4e6905aca341b15a566f35a10dd8d1e93f5dbb3b7"},
+ {file = "greenlet-3.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6512592cc49b2c6d9b19fbaa0312124cd4c4c8a90d28473f86f92685cc5fef8e"},
+ {file = "greenlet-3.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:871b0a8835f9e9d461b7fdaa1b57e3492dd45398e87324c047469ce2fc9f516c"},
+ {file = "greenlet-3.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b505fcfc26f4148551826a96f7317e02c400665fa0883fe505d4fcaab1dabfdd"},
+ {file = "greenlet-3.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:123910c58234a8d40eaab595bc56a5ae49bdd90122dde5bdc012c20595a94c14"},
+ {file = "greenlet-3.0.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96d9ea57292f636ec851a9bb961a5cc0f9976900e16e5d5647f19aa36ba6366b"},
+ {file = "greenlet-3.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0b72b802496cccbd9b31acea72b6f87e7771ccfd7f7927437d592e5c92ed703c"},
+ {file = "greenlet-3.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:527cd90ba3d8d7ae7dceb06fda619895768a46a1b4e423bdb24c1969823b8362"},
+ {file = "greenlet-3.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:37f60b3a42d8b5499be910d1267b24355c495064f271cfe74bf28b17b099133c"},
+ {file = "greenlet-3.0.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1482fba7fbed96ea7842b5a7fc11d61727e8be75a077e603e8ab49d24e234383"},
+ {file = "greenlet-3.0.0-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:be557119bf467d37a8099d91fbf11b2de5eb1fd5fc5b91598407574848dc910f"},
+ {file = "greenlet-3.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73b2f1922a39d5d59cc0e597987300df3396b148a9bd10b76a058a2f2772fc04"},
+ {file = "greenlet-3.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1e22c22f7826096ad503e9bb681b05b8c1f5a8138469b255eb91f26a76634f2"},
+ {file = "greenlet-3.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1d363666acc21d2c204dd8705c0e0457d7b2ee7a76cb16ffc099d6799744ac99"},
+ {file = "greenlet-3.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:334ef6ed8337bd0b58bb0ae4f7f2dcc84c9f116e474bb4ec250a8bb9bd797a66"},
+ {file = "greenlet-3.0.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6672fdde0fd1a60b44fb1751a7779c6db487e42b0cc65e7caa6aa686874e79fb"},
+ {file = "greenlet-3.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:952256c2bc5b4ee8df8dfc54fc4de330970bf5d79253c863fb5e6761f00dda35"},
+ {file = "greenlet-3.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:269d06fa0f9624455ce08ae0179430eea61085e3cf6457f05982b37fd2cefe17"},
+ {file = "greenlet-3.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:9adbd8ecf097e34ada8efde9b6fec4dd2a903b1e98037adf72d12993a1c80b51"},
+ {file = "greenlet-3.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6b5ce7f40f0e2f8b88c28e6691ca6806814157ff05e794cdd161be928550f4c"},
+ {file = "greenlet-3.0.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ecf94aa539e97a8411b5ea52fc6ccd8371be9550c4041011a091eb8b3ca1d810"},
+ {file = "greenlet-3.0.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80dcd3c938cbcac986c5c92779db8e8ce51a89a849c135172c88ecbdc8c056b7"},
+ {file = "greenlet-3.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e52a712c38e5fb4fd68e00dc3caf00b60cb65634d50e32281a9d6431b33b4af1"},
+ {file = "greenlet-3.0.0-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5539f6da3418c3dc002739cb2bb8d169056aa66e0c83f6bacae0cd3ac26b423"},
+ {file = "greenlet-3.0.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:343675e0da2f3c69d3fb1e894ba0a1acf58f481f3b9372ce1eb465ef93cf6fed"},
+ {file = "greenlet-3.0.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:abe1ef3d780de56defd0c77c5ba95e152f4e4c4e12d7e11dd8447d338b85a625"},
+ {file = "greenlet-3.0.0-cp37-cp37m-win32.whl", hash = "sha256:e693e759e172fa1c2c90d35dea4acbdd1d609b6936115d3739148d5e4cd11947"},
+ {file = "greenlet-3.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:bdd696947cd695924aecb3870660b7545a19851f93b9d327ef8236bfc49be705"},
+ {file = "greenlet-3.0.0-cp37-universal2-macosx_11_0_x86_64.whl", hash = "sha256:cc3e2679ea13b4de79bdc44b25a0c4fcd5e94e21b8f290791744ac42d34a0353"},
+ {file = "greenlet-3.0.0-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:63acdc34c9cde42a6534518e32ce55c30f932b473c62c235a466469a710bfbf9"},
+ {file = "greenlet-3.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a1a6244ff96343e9994e37e5b4839f09a0207d35ef6134dce5c20d260d0302c"},
+ {file = "greenlet-3.0.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b822fab253ac0f330ee807e7485769e3ac85d5eef827ca224feaaefa462dc0d0"},
+ {file = "greenlet-3.0.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8060b32d8586e912a7b7dac2d15b28dbbd63a174ab32f5bc6d107a1c4143f40b"},
+ {file = "greenlet-3.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:621fcb346141ae08cb95424ebfc5b014361621b8132c48e538e34c3c93ac7365"},
+ {file = "greenlet-3.0.0-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6bb36985f606a7c49916eff74ab99399cdfd09241c375d5a820bb855dfb4af9f"},
+ {file = "greenlet-3.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:10b5582744abd9858947d163843d323d0b67be9432db50f8bf83031032bc218d"},
+ {file = "greenlet-3.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f351479a6914fd81a55c8e68963609f792d9b067fb8a60a042c585a621e0de4f"},
+ {file = "greenlet-3.0.0-cp38-cp38-win32.whl", hash = "sha256:9de687479faec7db5b198cc365bc34addd256b0028956501f4d4d5e9ca2e240a"},
+ {file = "greenlet-3.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:3fd2b18432e7298fcbec3d39e1a0aa91ae9ea1c93356ec089421fabc3651572b"},
+ {file = "greenlet-3.0.0-cp38-universal2-macosx_11_0_x86_64.whl", hash = "sha256:3c0d36f5adc6e6100aedbc976d7428a9f7194ea79911aa4bf471f44ee13a9464"},
+ {file = "greenlet-3.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4cd83fb8d8e17633ad534d9ac93719ef8937568d730ef07ac3a98cb520fd93e4"},
+ {file = "greenlet-3.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a5b2d4cdaf1c71057ff823a19d850ed5c6c2d3686cb71f73ae4d6382aaa7a06"},
+ {file = "greenlet-3.0.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e7dcdfad252f2ca83c685b0fa9fba00e4d8f243b73839229d56ee3d9d219314"},
+ {file = "greenlet-3.0.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c94e4e924d09b5a3e37b853fe5924a95eac058cb6f6fb437ebb588b7eda79870"},
+ {file = "greenlet-3.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad6fb737e46b8bd63156b8f59ba6cdef46fe2b7db0c5804388a2d0519b8ddb99"},
+ {file = "greenlet-3.0.0-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d55db1db455c59b46f794346efce896e754b8942817f46a1bada2d29446e305a"},
+ {file = "greenlet-3.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:56867a3b3cf26dc8a0beecdb4459c59f4c47cdd5424618c08515f682e1d46692"},
+ {file = "greenlet-3.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9a812224a5fb17a538207e8cf8e86f517df2080c8ee0f8c1ed2bdaccd18f38f4"},
+ {file = "greenlet-3.0.0-cp39-cp39-win32.whl", hash = "sha256:0d3f83ffb18dc57243e0151331e3c383b05e5b6c5029ac29f754745c800f8ed9"},
+ {file = "greenlet-3.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:831d6f35037cf18ca5e80a737a27d822d87cd922521d18ed3dbc8a6967be50ce"},
+ {file = "greenlet-3.0.0-cp39-universal2-macosx_11_0_x86_64.whl", hash = "sha256:a048293392d4e058298710a54dfaefcefdf49d287cd33fb1f7d63d55426e4355"},
+ {file = "greenlet-3.0.0.tar.gz", hash = "sha256:19834e3f91f485442adc1ee440171ec5d9a4840a1f7bd5ed97833544719ce10b"},
]
[package.extras]
-docs = ["Sphinx", "docutils (<0.18)"]
+docs = ["Sphinx"]
test = ["objgraph", "psutil"]
[[package]]
@@ -1156,13 +1192,13 @@ lxml = ["lxml"]
[[package]]
name = "httpcore"
-version = "0.17.3"
+version = "0.18.0"
description = "A minimal low-level HTTP client."
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
files = [
- {file = "httpcore-0.17.3-py3-none-any.whl", hash = "sha256:c2789b767ddddfa2a5782e3199b2b7f6894540b17b16ec26b2c4d8e103510b87"},
- {file = "httpcore-0.17.3.tar.gz", hash = "sha256:a6f30213335e34c1ade7be6ec7c47f19f50c56db36abef1a9dfa3815b1cb3888"},
+ {file = "httpcore-0.18.0-py3-none-any.whl", hash = "sha256:adc5398ee0a476567bf87467063ee63584a8bce86078bf748e48754f60202ced"},
+ {file = "httpcore-0.18.0.tar.gz", hash = "sha256:13b5e5cd1dca1a6636a6aaea212b19f4f85cd88c366a2b82304181b769aab3c9"},
]
[package.dependencies]
@@ -1177,18 +1213,18 @@ socks = ["socksio (==1.*)"]
[[package]]
name = "httpx"
-version = "0.24.1"
+version = "0.25.0"
description = "The next generation HTTP client."
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
files = [
- {file = "httpx-0.24.1-py3-none-any.whl", hash = "sha256:06781eb9ac53cde990577af654bd990a4949de37a28bdb4a230d434f3a30b9bd"},
- {file = "httpx-0.24.1.tar.gz", hash = "sha256:5853a43053df830c20f8110c5e69fe44d035d850b2dfe795e196f00fdb774bdd"},
+ {file = "httpx-0.25.0-py3-none-any.whl", hash = "sha256:181ea7f8ba3a82578be86ef4171554dd45fec26a02556a744db029a0a27b7100"},
+ {file = "httpx-0.25.0.tar.gz", hash = "sha256:47ecda285389cb32bb2691cc6e069e3ab0205956f681c5b2ad2325719751d875"},
]
[package.dependencies]
certifi = "*"
-httpcore = ">=0.15.0,<0.18.0"
+httpcore = ">=0.18.0,<0.19.0"
idna = "*"
sniffio = "*"
@@ -1347,6 +1383,25 @@ win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""}
[package.extras]
dev = ["Sphinx (==7.2.5)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptiongroup (==1.1.3)", "freezegun (==1.1.0)", "freezegun (==1.2.2)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v1.4.1)", "mypy (==v1.5.1)", "pre-commit (==3.4.0)", "pytest (==6.1.2)", "pytest (==7.4.0)", "pytest-cov (==2.12.1)", "pytest-cov (==4.1.0)", "pytest-mypy-plugins (==1.9.3)", "pytest-mypy-plugins (==3.0.0)", "sphinx-autobuild (==2021.3.14)", "sphinx-rtd-theme (==1.3.0)", "tox (==3.27.1)", "tox (==4.11.0)"]
+[[package]]
+name = "mako"
+version = "1.2.4"
+description = "A super-fast templating language that borrows the best ideas from the existing templating languages."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "Mako-1.2.4-py3-none-any.whl", hash = "sha256:c97c79c018b9165ac9922ae4f32da095ffd3c4e6872b45eded42926deea46818"},
+ {file = "Mako-1.2.4.tar.gz", hash = "sha256:d60a3903dc3bb01a18ad6a89cdbe2e4eadc69c0bc8ef1e3773ba53d44c3f7a34"},
+]
+
+[package.dependencies]
+MarkupSafe = ">=0.9.2"
+
+[package.extras]
+babel = ["Babel"]
+lingua = ["lingua"]
+testing = ["pytest"]
+
[[package]]
name = "markdown-it-py"
version = "3.0.0"
@@ -1551,6 +1606,89 @@ files = [
{file = "msgpack-1.0.7.tar.gz", hash = "sha256:572efc93db7a4d27e404501975ca6d2d9775705c2d922390d878fcf768d92c87"},
]
+[[package]]
+name = "multidict"
+version = "6.0.4"
+description = "multidict implementation"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8"},
+ {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171"},
+ {file = "multidict-6.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7"},
+ {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b"},
+ {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547"},
+ {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569"},
+ {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93"},
+ {file = "multidict-6.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98"},
+ {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0"},
+ {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988"},
+ {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc"},
+ {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0"},
+ {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5"},
+ {file = "multidict-6.0.4-cp310-cp310-win32.whl", hash = "sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8"},
+ {file = "multidict-6.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc"},
+ {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03"},
+ {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3"},
+ {file = "multidict-6.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba"},
+ {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9"},
+ {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982"},
+ {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe"},
+ {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710"},
+ {file = "multidict-6.0.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c"},
+ {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4"},
+ {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a"},
+ {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c"},
+ {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed"},
+ {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461"},
+ {file = "multidict-6.0.4-cp311-cp311-win32.whl", hash = "sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636"},
+ {file = "multidict-6.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0"},
+ {file = "multidict-6.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78"},
+ {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f"},
+ {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603"},
+ {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac"},
+ {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9"},
+ {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2"},
+ {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde"},
+ {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe"},
+ {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067"},
+ {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87"},
+ {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d"},
+ {file = "multidict-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775"},
+ {file = "multidict-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e"},
+ {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c"},
+ {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161"},
+ {file = "multidict-6.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11"},
+ {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e"},
+ {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d"},
+ {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2"},
+ {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258"},
+ {file = "multidict-6.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52"},
+ {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660"},
+ {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951"},
+ {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60"},
+ {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d"},
+ {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1"},
+ {file = "multidict-6.0.4-cp38-cp38-win32.whl", hash = "sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779"},
+ {file = "multidict-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480"},
+ {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664"},
+ {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35"},
+ {file = "multidict-6.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60"},
+ {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706"},
+ {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d"},
+ {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca"},
+ {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1"},
+ {file = "multidict-6.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449"},
+ {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf"},
+ {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063"},
+ {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a"},
+ {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176"},
+ {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95"},
+ {file = "multidict-6.0.4-cp39-cp39-win32.whl", hash = "sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313"},
+ {file = "multidict-6.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2"},
+ {file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"},
+]
+
[[package]]
name = "mypy"
version = "1.5.1"
@@ -2474,33 +2612,33 @@ cli = ["click (>=5.0)"]
[[package]]
name = "python-telegram-bot"
-version = "20.5"
+version = "20.6"
description = "We have made you a wrapper you can't refuse"
optional = false
python-versions = ">=3.8"
files = [
- {file = "python-telegram-bot-20.5.tar.gz", hash = "sha256:2f45a94c861cbd40440ece2be176ef0fc69e10d84e6dfa17f9a456e32aeece13"},
- {file = "python_telegram_bot-20.5-py3-none-any.whl", hash = "sha256:fc9605a855794231c802cc3948e6f7c319a817b5cd1827371f170bc7ca0ca279"},
+ {file = "python-telegram-bot-20.6.tar.gz", hash = "sha256:c6951dc6b9368d80a17b5d0496ced39bca2c9bafab06cbfdae6226493d8b06fe"},
+ {file = "python_telegram_bot-20.6-py3-none-any.whl", hash = "sha256:be8ebda350fd0c69c3ebdd7ce87b1e6f8b3fb1d218a193dd821d1d6285b9f649"},
]
[package.dependencies]
aiolimiter = {version = ">=1.1.0,<1.2.0", optional = true, markers = "extra == \"ext\""}
APScheduler = {version = ">=3.10.4,<3.11.0", optional = true, markers = "extra == \"ext\""}
cachetools = {version = ">=5.3.1,<5.4.0", optional = true, markers = "extra == \"ext\""}
-httpx = ">=0.24.1,<0.25.0"
+httpx = ">=0.25.0,<0.26.0"
pytz = {version = ">=2018.6", optional = true, markers = "extra == \"ext\""}
-tornado = {version = ">=6.2,<7.0", optional = true, markers = "extra == \"ext\""}
+tornado = {version = ">=6.3.3,<6.4.0", optional = true, markers = "extra == \"ext\""}
[package.extras]
-all = ["APScheduler (>=3.10.4,<3.11.0)", "aiolimiter (>=1.1.0,<1.2.0)", "cachetools (>=5.3.1,<5.4.0)", "cryptography (>=39.0.1)", "httpx[http2]", "httpx[socks]", "pytz (>=2018.6)", "tornado (>=6.2,<7.0)"]
+all = ["APScheduler (>=3.10.4,<3.11.0)", "aiolimiter (>=1.1.0,<1.2.0)", "cachetools (>=5.3.1,<5.4.0)", "cryptography (>=39.0.1)", "httpx[http2]", "httpx[socks]", "pytz (>=2018.6)", "tornado (>=6.3.3,<6.4.0)"]
callback-data = ["cachetools (>=5.3.1,<5.4.0)"]
-ext = ["APScheduler (>=3.10.4,<3.11.0)", "aiolimiter (>=1.1.0,<1.2.0)", "cachetools (>=5.3.1,<5.4.0)", "pytz (>=2018.6)", "tornado (>=6.2,<7.0)"]
+ext = ["APScheduler (>=3.10.4,<3.11.0)", "aiolimiter (>=1.1.0,<1.2.0)", "cachetools (>=5.3.1,<5.4.0)", "pytz (>=2018.6)", "tornado (>=6.3.3,<6.4.0)"]
http2 = ["httpx[http2]"]
job-queue = ["APScheduler (>=3.10.4,<3.11.0)", "pytz (>=2018.6)"]
passport = ["cryptography (>=39.0.1)"]
rate-limiter = ["aiolimiter (>=1.1.0,<1.2.0)"]
socks = ["httpx[socks]"]
-webhooks = ["tornado (>=6.2,<7.0)"]
+webhooks = ["tornado (>=6.3.3,<6.4.0)"]
[[package]]
name = "pytz"
@@ -2631,17 +2769,17 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]]
name = "ruamel-yaml"
-version = "0.17.33"
+version = "0.17.35"
description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order"
optional = false
python-versions = ">=3"
files = [
- {file = "ruamel.yaml-0.17.33-py3-none-any.whl", hash = "sha256:2080c7a02b8a30fb3c06727cdf3e254a64055eedf3aa2d17c2b669639c04971b"},
- {file = "ruamel.yaml-0.17.33.tar.gz", hash = "sha256:5c56aa0bff2afceaa93bffbfc78b450b7dc1e01d5edb80b3a570695286ae62b1"},
+ {file = "ruamel.yaml-0.17.35-py3-none-any.whl", hash = "sha256:b105e3e6fc15b41fdb201ba1b95162ae566a4ef792b9f884c46b4ccc5513a87a"},
+ {file = "ruamel.yaml-0.17.35.tar.gz", hash = "sha256:801046a9caacb1b43acc118969b49b96b65e8847f29029563b29ac61d02db61b"},
]
[package.dependencies]
-"ruamel.yaml.clib" = {version = ">=0.2.7", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.12\""}
+"ruamel.yaml.clib" = {version = ">=0.2.7", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.13\""}
[package.extras]
docs = ["ryd"]
@@ -2834,6 +2972,85 @@ requests = ">=2.26.0"
[package.extras]
whisper-api = ["openai"]
+[[package]]
+name = "sqlalchemy"
+version = "2.0.21"
+description = "Database Abstraction Library"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "SQLAlchemy-2.0.21-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1e7dc99b23e33c71d720c4ae37ebb095bebebbd31a24b7d99dfc4753d2803ede"},
+ {file = "SQLAlchemy-2.0.21-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7f0c4ee579acfe6c994637527c386d1c22eb60bc1c1d36d940d8477e482095d4"},
+ {file = "SQLAlchemy-2.0.21-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f7d57a7e140efe69ce2d7b057c3f9a595f98d0bbdfc23fd055efdfbaa46e3a5"},
+ {file = "SQLAlchemy-2.0.21-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ca38746eac23dd7c20bec9278d2058c7ad662b2f1576e4c3dbfcd7c00cc48fa"},
+ {file = "SQLAlchemy-2.0.21-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3cf229704074bce31f7f47d12883afee3b0a02bb233a0ba45ddbfe542939cca4"},
+ {file = "SQLAlchemy-2.0.21-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fb87f763b5d04a82ae84ccff25554ffd903baafba6698e18ebaf32561f2fe4aa"},
+ {file = "SQLAlchemy-2.0.21-cp310-cp310-win32.whl", hash = "sha256:89e274604abb1a7fd5c14867a412c9d49c08ccf6ce3e1e04fffc068b5b6499d4"},
+ {file = "SQLAlchemy-2.0.21-cp310-cp310-win_amd64.whl", hash = "sha256:e36339a68126ffb708dc6d1948161cea2a9e85d7d7b0c54f6999853d70d44430"},
+ {file = "SQLAlchemy-2.0.21-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bf8eebccc66829010f06fbd2b80095d7872991bfe8415098b9fe47deaaa58063"},
+ {file = "SQLAlchemy-2.0.21-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b977bfce15afa53d9cf6a632482d7968477625f030d86a109f7bdfe8ce3c064a"},
+ {file = "SQLAlchemy-2.0.21-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ff3dc2f60dbf82c9e599c2915db1526d65415be323464f84de8db3e361ba5b9"},
+ {file = "SQLAlchemy-2.0.21-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44ac5c89b6896f4740e7091f4a0ff2e62881da80c239dd9408f84f75a293dae9"},
+ {file = "SQLAlchemy-2.0.21-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:87bf91ebf15258c4701d71dcdd9c4ba39521fb6a37379ea68088ce8cd869b446"},
+ {file = "SQLAlchemy-2.0.21-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b69f1f754d92eb1cc6b50938359dead36b96a1dcf11a8670bff65fd9b21a4b09"},
+ {file = "SQLAlchemy-2.0.21-cp311-cp311-win32.whl", hash = "sha256:af520a730d523eab77d754f5cf44cc7dd7ad2d54907adeb3233177eeb22f271b"},
+ {file = "SQLAlchemy-2.0.21-cp311-cp311-win_amd64.whl", hash = "sha256:141675dae56522126986fa4ca713739d00ed3a6f08f3c2eb92c39c6dfec463ce"},
+ {file = "SQLAlchemy-2.0.21-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7614f1eab4336df7dd6bee05bc974f2b02c38d3d0c78060c5faa4cd1ca2af3b8"},
+ {file = "SQLAlchemy-2.0.21-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d59cb9e20d79686aa473e0302e4a82882d7118744d30bb1dfb62d3c47141b3ec"},
+ {file = "SQLAlchemy-2.0.21-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a95aa0672e3065d43c8aa80080cdd5cc40fe92dc873749e6c1cf23914c4b83af"},
+ {file = "SQLAlchemy-2.0.21-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8c323813963b2503e54d0944813cd479c10c636e3ee223bcbd7bd478bf53c178"},
+ {file = "SQLAlchemy-2.0.21-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:419b1276b55925b5ac9b4c7044e999f1787c69761a3c9756dec6e5c225ceca01"},
+ {file = "SQLAlchemy-2.0.21-cp37-cp37m-win32.whl", hash = "sha256:4615623a490e46be85fbaa6335f35cf80e61df0783240afe7d4f544778c315a9"},
+ {file = "SQLAlchemy-2.0.21-cp37-cp37m-win_amd64.whl", hash = "sha256:cca720d05389ab1a5877ff05af96551e58ba65e8dc65582d849ac83ddde3e231"},
+ {file = "SQLAlchemy-2.0.21-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b4eae01faee9f2b17f08885e3f047153ae0416648f8e8c8bd9bc677c5ce64be9"},
+ {file = "SQLAlchemy-2.0.21-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3eb7c03fe1cd3255811cd4e74db1ab8dca22074d50cd8937edf4ef62d758cdf4"},
+ {file = "SQLAlchemy-2.0.21-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2d494b6a2a2d05fb99f01b84cc9af9f5f93bf3e1e5dbdafe4bed0c2823584c1"},
+ {file = "SQLAlchemy-2.0.21-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b19ae41ef26c01a987e49e37c77b9ad060c59f94d3b3efdfdbf4f3daaca7b5fe"},
+ {file = "SQLAlchemy-2.0.21-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:fc6b15465fabccc94bf7e38777d665b6a4f95efd1725049d6184b3a39fd54880"},
+ {file = "SQLAlchemy-2.0.21-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:014794b60d2021cc8ae0f91d4d0331fe92691ae5467a00841f7130fe877b678e"},
+ {file = "SQLAlchemy-2.0.21-cp38-cp38-win32.whl", hash = "sha256:0268256a34806e5d1c8f7ee93277d7ea8cc8ae391f487213139018b6805aeaf6"},
+ {file = "SQLAlchemy-2.0.21-cp38-cp38-win_amd64.whl", hash = "sha256:73c079e21d10ff2be54a4699f55865d4b275fd6c8bd5d90c5b1ef78ae0197301"},
+ {file = "SQLAlchemy-2.0.21-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:785e2f2c1cb50d0a44e2cdeea5fd36b5bf2d79c481c10f3a88a8be4cfa2c4615"},
+ {file = "SQLAlchemy-2.0.21-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c111cd40910ffcb615b33605fc8f8e22146aeb7933d06569ac90f219818345ef"},
+ {file = "SQLAlchemy-2.0.21-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9cba4e7369de663611ce7460a34be48e999e0bbb1feb9130070f0685e9a6b66"},
+ {file = "SQLAlchemy-2.0.21-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50a69067af86ec7f11a8e50ba85544657b1477aabf64fa447fd3736b5a0a4f67"},
+ {file = "SQLAlchemy-2.0.21-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ccb99c3138c9bde118b51a289d90096a3791658da9aea1754667302ed6564f6e"},
+ {file = "SQLAlchemy-2.0.21-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:513fd5b6513d37e985eb5b7ed89da5fd9e72354e3523980ef00d439bc549c9e9"},
+ {file = "SQLAlchemy-2.0.21-cp39-cp39-win32.whl", hash = "sha256:f9fefd6298433b6e9188252f3bff53b9ff0443c8fde27298b8a2b19f6617eeb9"},
+ {file = "SQLAlchemy-2.0.21-cp39-cp39-win_amd64.whl", hash = "sha256:2e617727fe4091cedb3e4409b39368f424934c7faa78171749f704b49b4bb4ce"},
+ {file = "SQLAlchemy-2.0.21-py3-none-any.whl", hash = "sha256:ea7da25ee458d8f404b93eb073116156fd7d8c2a776d8311534851f28277b4ce"},
+ {file = "SQLAlchemy-2.0.21.tar.gz", hash = "sha256:05b971ab1ac2994a14c56b35eaaa91f86ba080e9ad481b20d99d77f381bb6258"},
+]
+
+[package.dependencies]
+greenlet = {version = "!=0.4.17", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""}
+mypy = {version = ">=0.910", optional = true, markers = "extra == \"mypy\""}
+typing-extensions = ">=4.2.0"
+
+[package.extras]
+aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"]
+aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing-extensions (!=3.10.0.1)"]
+asyncio = ["greenlet (!=0.4.17)"]
+asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"]
+mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"]
+mssql = ["pyodbc"]
+mssql-pymssql = ["pymssql"]
+mssql-pyodbc = ["pyodbc"]
+mypy = ["mypy (>=0.910)"]
+mysql = ["mysqlclient (>=1.4.0)"]
+mysql-connector = ["mysql-connector-python"]
+oracle = ["cx-oracle (>=7)"]
+oracle-oracledb = ["oracledb (>=1.0.1)"]
+postgresql = ["psycopg2 (>=2.7)"]
+postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"]
+postgresql-pg8000 = ["pg8000 (>=1.29.1)"]
+postgresql-psycopg = ["psycopg (>=3.0.7)"]
+postgresql-psycopg2binary = ["psycopg2-binary"]
+postgresql-psycopg2cffi = ["psycopg2cffi"]
+postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"]
+pymysql = ["pymysql"]
+sqlcipher = ["sqlcipher3-binary"]
+
[[package]]
name = "stack-data"
version = "0.6.3"
@@ -2953,13 +3170,13 @@ files = [
[[package]]
name = "traitlets"
-version = "5.11.1"
+version = "5.11.2"
description = "Traitlets Python configuration system"
optional = false
python-versions = ">=3.8"
files = [
- {file = "traitlets-5.11.1-py3-none-any.whl", hash = "sha256:2351372ff87fc912c483d1cb6aa466573d5f44eb4ed9e602c8d0ac012c9daece"},
- {file = "traitlets-5.11.1.tar.gz", hash = "sha256:813584bb569ac4a098c64ca494e8a3bbfef42e37c36a6132bca554aabd111480"},
+ {file = "traitlets-5.11.2-py3-none-any.whl", hash = "sha256:98277f247f18b2c5cabaf4af369187754f4fb0e85911d473f72329db8a7f4fae"},
+ {file = "traitlets-5.11.2.tar.gz", hash = "sha256:7564b5bf8d38c40fa45498072bf4dc5e8346eb087bbf1e2ae2d8774f6a0f078e"},
]
[package.extras]
@@ -3012,13 +3229,13 @@ files = [
[[package]]
name = "tzlocal"
-version = "5.0.1"
+version = "5.1"
description = "tzinfo object for the local timezone"
optional = false
python-versions = ">=3.7"
files = [
- {file = "tzlocal-5.0.1-py3-none-any.whl", hash = "sha256:f3596e180296aaf2dbd97d124fe76ae3a0e3d32b258447de7b939b3fd4be992f"},
- {file = "tzlocal-5.0.1.tar.gz", hash = "sha256:46eb99ad4bdb71f3f72b7d24f4267753e240944ecfc16f25d2719ba89827a803"},
+ {file = "tzlocal-5.1-py3-none-any.whl", hash = "sha256:2938498395d5f6a898ab8009555cb37a4d360913ad375d4747ef16826b03ef23"},
+ {file = "tzlocal-5.1.tar.gz", hash = "sha256:a5ccb2365b295ed964e0a98ad076fe10c495591e75505d34f154d60a7f1ed722"},
]
[package.dependencies]
@@ -3130,7 +3347,94 @@ pyyaml = "*"
[package.extras]
dev = ["doc8", "flake8", "flake8-import-order", "rstcheck[sphinx]", "sphinx"]
+[[package]]
+name = "yarl"
+version = "1.9.2"
+description = "Yet another URL library"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "yarl-1.9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8c2ad583743d16ddbdf6bb14b5cd76bf43b0d0006e918809d5d4ddf7bde8dd82"},
+ {file = "yarl-1.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:82aa6264b36c50acfb2424ad5ca537a2060ab6de158a5bd2a72a032cc75b9eb8"},
+ {file = "yarl-1.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c0c77533b5ed4bcc38e943178ccae29b9bcf48ffd1063f5821192f23a1bd27b9"},
+ {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee4afac41415d52d53a9833ebae7e32b344be72835bbb589018c9e938045a560"},
+ {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bf345c3a4f5ba7f766430f97f9cc1320786f19584acc7086491f45524a551ac"},
+ {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a96c19c52ff442a808c105901d0bdfd2e28575b3d5f82e2f5fd67e20dc5f4ea"},
+ {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:891c0e3ec5ec881541f6c5113d8df0315ce5440e244a716b95f2525b7b9f3608"},
+ {file = "yarl-1.9.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c3a53ba34a636a256d767c086ceb111358876e1fb6b50dfc4d3f4951d40133d5"},
+ {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:566185e8ebc0898b11f8026447eacd02e46226716229cea8db37496c8cdd26e0"},
+ {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2b0738fb871812722a0ac2154be1f049c6223b9f6f22eec352996b69775b36d4"},
+ {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:32f1d071b3f362c80f1a7d322bfd7b2d11e33d2adf395cc1dd4df36c9c243095"},
+ {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:e9fdc7ac0d42bc3ea78818557fab03af6181e076a2944f43c38684b4b6bed8e3"},
+ {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:56ff08ab5df8429901ebdc5d15941b59f6253393cb5da07b4170beefcf1b2528"},
+ {file = "yarl-1.9.2-cp310-cp310-win32.whl", hash = "sha256:8ea48e0a2f931064469bdabca50c2f578b565fc446f302a79ba6cc0ee7f384d3"},
+ {file = "yarl-1.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:50f33040f3836e912ed16d212f6cc1efb3231a8a60526a407aeb66c1c1956dde"},
+ {file = "yarl-1.9.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:646d663eb2232d7909e6601f1a9107e66f9791f290a1b3dc7057818fe44fc2b6"},
+ {file = "yarl-1.9.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aff634b15beff8902d1f918012fc2a42e0dbae6f469fce134c8a0dc51ca423bb"},
+ {file = "yarl-1.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a83503934c6273806aed765035716216cc9ab4e0364f7f066227e1aaea90b8d0"},
+ {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b25322201585c69abc7b0e89e72790469f7dad90d26754717f3310bfe30331c2"},
+ {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:22a94666751778629f1ec4280b08eb11815783c63f52092a5953faf73be24191"},
+ {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ec53a0ea2a80c5cd1ab397925f94bff59222aa3cf9c6da938ce05c9ec20428d"},
+ {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:159d81f22d7a43e6eabc36d7194cb53f2f15f498dbbfa8edc8a3239350f59fe7"},
+ {file = "yarl-1.9.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:832b7e711027c114d79dffb92576acd1bd2decc467dec60e1cac96912602d0e6"},
+ {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:95d2ecefbcf4e744ea952d073c6922e72ee650ffc79028eb1e320e732898d7e8"},
+ {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d4e2c6d555e77b37288eaf45b8f60f0737c9efa3452c6c44626a5455aeb250b9"},
+ {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:783185c75c12a017cc345015ea359cc801c3b29a2966c2655cd12b233bf5a2be"},
+ {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:b8cc1863402472f16c600e3e93d542b7e7542a540f95c30afd472e8e549fc3f7"},
+ {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:822b30a0f22e588b32d3120f6d41e4ed021806418b4c9f0bc3048b8c8cb3f92a"},
+ {file = "yarl-1.9.2-cp311-cp311-win32.whl", hash = "sha256:a60347f234c2212a9f0361955007fcf4033a75bf600a33c88a0a8e91af77c0e8"},
+ {file = "yarl-1.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:be6b3fdec5c62f2a67cb3f8c6dbf56bbf3f61c0f046f84645cd1ca73532ea051"},
+ {file = "yarl-1.9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:38a3928ae37558bc1b559f67410df446d1fbfa87318b124bf5032c31e3447b74"},
+ {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac9bb4c5ce3975aeac288cfcb5061ce60e0d14d92209e780c93954076c7c4367"},
+ {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3da8a678ca8b96c8606bbb8bfacd99a12ad5dd288bc6f7979baddd62f71c63ef"},
+ {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13414591ff516e04fcdee8dc051c13fd3db13b673c7a4cb1350e6b2ad9639ad3"},
+ {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf74d08542c3a9ea97bb8f343d4fcbd4d8f91bba5ec9d5d7f792dbe727f88938"},
+ {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e7221580dc1db478464cfeef9b03b95c5852cc22894e418562997df0d074ccc"},
+ {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:494053246b119b041960ddcd20fd76224149cfea8ed8777b687358727911dd33"},
+ {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:52a25809fcbecfc63ac9ba0c0fb586f90837f5425edfd1ec9f3372b119585e45"},
+ {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:e65610c5792870d45d7b68c677681376fcf9cc1c289f23e8e8b39c1485384185"},
+ {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:1b1bba902cba32cdec51fca038fd53f8beee88b77efc373968d1ed021024cc04"},
+ {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:662e6016409828ee910f5d9602a2729a8a57d74b163c89a837de3fea050c7582"},
+ {file = "yarl-1.9.2-cp37-cp37m-win32.whl", hash = "sha256:f364d3480bffd3aa566e886587eaca7c8c04d74f6e8933f3f2c996b7f09bee1b"},
+ {file = "yarl-1.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6a5883464143ab3ae9ba68daae8e7c5c95b969462bbe42e2464d60e7e2698368"},
+ {file = "yarl-1.9.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5610f80cf43b6202e2c33ba3ec2ee0a2884f8f423c8f4f62906731d876ef4fac"},
+ {file = "yarl-1.9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b9a4e67ad7b646cd6f0938c7ebfd60e481b7410f574c560e455e938d2da8e0f4"},
+ {file = "yarl-1.9.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:83fcc480d7549ccebe9415d96d9263e2d4226798c37ebd18c930fce43dfb9574"},
+ {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fcd436ea16fee7d4207c045b1e340020e58a2597301cfbcfdbe5abd2356c2fb"},
+ {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84e0b1599334b1e1478db01b756e55937d4614f8654311eb26012091be109d59"},
+ {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3458a24e4ea3fd8930e934c129b676c27452e4ebda80fbe47b56d8c6c7a63a9e"},
+ {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:838162460b3a08987546e881a2bfa573960bb559dfa739e7800ceeec92e64417"},
+ {file = "yarl-1.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4e2d08f07a3d7d3e12549052eb5ad3eab1c349c53ac51c209a0e5991bbada78"},
+ {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:de119f56f3c5f0e2fb4dee508531a32b069a5f2c6e827b272d1e0ff5ac040333"},
+ {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:149ddea5abf329752ea5051b61bd6c1d979e13fbf122d3a1f9f0c8be6cb6f63c"},
+ {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:674ca19cbee4a82c9f54e0d1eee28116e63bc6fd1e96c43031d11cbab8b2afd5"},
+ {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:9b3152f2f5677b997ae6c804b73da05a39daa6a9e85a512e0e6823d81cdad7cc"},
+ {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5415d5a4b080dc9612b1b63cba008db84e908b95848369aa1da3686ae27b6d2b"},
+ {file = "yarl-1.9.2-cp38-cp38-win32.whl", hash = "sha256:f7a3d8146575e08c29ed1cd287068e6d02f1c7bdff8970db96683b9591b86ee7"},
+ {file = "yarl-1.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:63c48f6cef34e6319a74c727376e95626f84ea091f92c0250a98e53e62c77c72"},
+ {file = "yarl-1.9.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:75df5ef94c3fdc393c6b19d80e6ef1ecc9ae2f4263c09cacb178d871c02a5ba9"},
+ {file = "yarl-1.9.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c027a6e96ef77d401d8d5a5c8d6bc478e8042f1e448272e8d9752cb0aff8b5c8"},
+ {file = "yarl-1.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3b078dbe227f79be488ffcfc7a9edb3409d018e0952cf13f15fd6512847f3f7"},
+ {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59723a029760079b7d991a401386390c4be5bfec1e7dd83e25a6a0881859e716"},
+ {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b03917871bf859a81ccb180c9a2e6c1e04d2f6a51d953e6a5cdd70c93d4e5a2a"},
+ {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1012fa63eb6c032f3ce5d2171c267992ae0c00b9e164efe4d73db818465fac3"},
+ {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a74dcbfe780e62f4b5a062714576f16c2f3493a0394e555ab141bf0d746bb955"},
+ {file = "yarl-1.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c56986609b057b4839968ba901944af91b8e92f1725d1a2d77cbac6972b9ed1"},
+ {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2c315df3293cd521033533d242d15eab26583360b58f7ee5d9565f15fee1bef4"},
+ {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:b7232f8dfbd225d57340e441d8caf8652a6acd06b389ea2d3222b8bc89cbfca6"},
+ {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:53338749febd28935d55b41bf0bcc79d634881195a39f6b2f767870b72514caf"},
+ {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:066c163aec9d3d073dc9ffe5dd3ad05069bcb03fcaab8d221290ba99f9f69ee3"},
+ {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8288d7cd28f8119b07dd49b7230d6b4562f9b61ee9a4ab02221060d21136be80"},
+ {file = "yarl-1.9.2-cp39-cp39-win32.whl", hash = "sha256:b124e2a6d223b65ba8768d5706d103280914d61f5cae3afbc50fc3dfcc016623"},
+ {file = "yarl-1.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:61016e7d582bc46a5378ffdd02cd0314fb8ba52f40f9cf4d9a5e7dbef88dee18"},
+ {file = "yarl-1.9.2.tar.gz", hash = "sha256:04ab9d4b9f587c06d801c2abfe9317b77cdf996c65a90d5e84ecc45010823571"},
+]
+
+[package.dependencies]
+idna = ">=2.0"
+multidict = ">=4.0"
+
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
-content-hash = "ab644b9882ee200392911afc6b71bf87fdb413e4fdd9f06a460ce33da98687d7"
+content-hash = "15f814322c6f006b5345d8579bb42198bb7c4953d595ea9662f9e7cae78a979f"
diff --git a/pyproject.toml b/pyproject.toml
index eb5b148..fbb1201 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -8,9 +8,9 @@ authors = ["Dmitry Afanasyev "]
python = "^3.11"
fastapi = "^0.103.1"
-python-telegram-bot = {version = "^20.5", extras=["ext"]}
+python-telegram-bot = {version = "^20.6", extras=["ext"]}
python-dotenv = "^1.0"
-httpx = "^0.24"
+httpx = "^0.25"
loguru = "^0.7"
pydantic = "^2.4"
pydantic-settings = "^2.0.3"
@@ -21,8 +21,12 @@ orjson = "^3.9"
sentry-sdk = "^1.31.0"
SpeechRecognition = "^3.8"
pydub = "^0.25"
-greenlet = "^2.0.2"
+greenlet = "^3.0"
graypy = "^2.1.0"
+aiosqlite = "^0.19.0"
+yarl = "^1.9.2"
+sqlalchemy = {version = "^2.0.21", extras=["mypy"]}
+alembic = "^1.12.0"
[tool.poetry.dev-dependencies]
@@ -159,7 +163,11 @@ warn_unused_configs = true
warn_unreachable = true
warn_no_return = true
exclude = [
- "chat_gpt_microservice"
+ "chat_gpt_microservice",
+ "bot_microservice/infra/database/migrations/versions/*"
+]
+plugins = [
+ "sqlalchemy.ext.mypy.plugin",
]
[tool.black]
diff --git a/scripts/start-bot.sh b/scripts/start-bot.sh
index 90e5cb3..c68bf5e 100644
--- a/scripts/start-bot.sh
+++ b/scripts/start-bot.sh
@@ -1,5 +1,9 @@
#! /bin/bash
+set -e
+
+alembic upgrade "head"
+
echo "starting the bot"
if [[ "${START_WITH_WEBHOOK}" == "true" ]]