diff --git a/.github/workflows/backend_check.yml b/.github/workflows/backend_check.yml index ce2b77ea..72c3e217 100644 --- a/.github/workflows/backend_check.yml +++ b/.github/workflows/backend_check.yml @@ -75,13 +75,9 @@ jobs: python -m poetry run chatsky.ui init --destination ../ --no-input --overwrite-if-exists working-directory: backend - - name: Install chatsky-ui into new project poetry environment - run: | - ../bin/add_ui_to_toml.sh - working-directory: my_project - - name: run tests run: | - python -m poetry install --no-root - python -m poetry run pytest ../backend/chatsky_ui/tests/ --verbose + poetry install --with dev -C ../backend + POETRY_ENV=$(poetry env info --path -C ../backend) + $POETRY_ENV/bin/pytest ../backend/chatsky_ui/tests/ --verbose working-directory: my_project diff --git a/.github/workflows/e2e_test.yml b/.github/workflows/e2e_test.yml index a0ae24bb..0cadf9c3 100644 --- a/.github/workflows/e2e_test.yml +++ b/.github/workflows/e2e_test.yml @@ -5,6 +5,7 @@ # push: # branches: # - dev +# - master # pull_request: # branches: # - dev diff --git a/Dockerfile b/Dockerfile index bc162f58..73271e69 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,7 +33,6 @@ ENV PATH="${PATH}:${POETRY_VENV}/bin" COPY ./backend /temp/backend COPY --from=frontend-builder /temp/frontend/dist /temp/backend/chatsky_ui/static - # Build the wheel WORKDIR /temp/backend RUN poetry build @@ -47,6 +46,9 @@ ARG PROJECT_DIR # Install pip and upgrade RUN pip install --upgrade pip +# Install Git +RUN apt-get update && apt-get install -y git + # Copy only the necessary files COPY --from=backend-builder /temp/backend/dist /src/dist COPY ./${PROJECT_DIR} /src/project_dir diff --git a/Makefile b/Makefile index 05b58031..62ec2158 100644 --- a/Makefile +++ b/Makefile @@ -153,7 +153,7 @@ init_proj: install_backend_env ## Initiates a new project using chatsky-ui .PHONY: init_with_cc init_with_cc: ## Initiates a new project using cookiecutter - cookiecutter https://github.com/Ramimashkouk/df_d_template.git + cookiecutter https://github.com/deeppavlov/chatsky-ui-template.git .PHONY: build_docs diff --git a/README.md b/README.md index fce37cbc..c46a7b1c 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # Quick Start ## System Requirements -Ensure you have Python version 3.8.1 or higher installed. +Ensure you have Python version 3.9 or higher installed (Excluding 3.9.7). ## Installation -To install the necessary package, run the following command: +To install the package and necessary dependencies, run the following command: ```bash pip install chatsky-ui ``` diff --git a/backend/chatsky_ui/api/api_v1/endpoints/bot.py b/backend/chatsky_ui/api/api_v1/endpoints/bot.py index a053f3b4..24a9d451 100644 --- a/backend/chatsky_ui/api/api_v1/endpoints/bot.py +++ b/backend/chatsky_ui/api/api_v1/endpoints/bot.py @@ -1,15 +1,14 @@ import asyncio from typing import Any, Dict, List, Optional, Union -from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, WebSocket, WebSocketException, status +from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, status +from httpx import AsyncClient from chatsky_ui.api import deps +from chatsky_ui.core.config import settings from chatsky_ui.schemas.pagination import Pagination from chatsky_ui.schemas.preset import Preset -from chatsky_ui.schemas.process_status import Status -from chatsky_ui.services.index import Index from chatsky_ui.services.process_manager import BuildManager, ProcessManager, RunManager -from chatsky_ui.services.websocket_manager import WebSocketManager router = APIRouter() @@ -45,7 +44,6 @@ async def start_build( preset: Preset, background_tasks: BackgroundTasks, build_manager: BuildManager = Depends(deps.get_build_manager), - index: Index = Depends(deps.get_index), ) -> Dict[str, Union[str, int]]: """Starts a `build` process with the given preset. @@ -61,7 +59,7 @@ async def start_build( await asyncio.sleep(preset.wait_time) build_id = await build_manager.start(preset) - background_tasks.add_task(build_manager.check_status, build_id, index) + background_tasks.add_task(build_manager.check_status, build_id) build_manager.logger.info("Build process '%s' has started", build_id) return {"status": "ok", "build_id": build_id} @@ -145,7 +143,7 @@ async def start_run( build_id: int, preset: Preset, background_tasks: BackgroundTasks, - run_manager: RunManager = Depends(deps.get_run_manager) + run_manager: RunManager = Depends(deps.get_run_manager), ) -> Dict[str, Union[str, int]]: """Starts a `run` process with the given preset. @@ -236,47 +234,20 @@ async def get_run_logs( return await run_manager.fetch_run_logs(run_id, pagination.offset(), pagination.limit) -@router.websocket("/run/connect") -async def connect( - websocket: WebSocket, - websocket_manager: WebSocketManager = Depends(deps.get_websocket_manager), - run_manager: RunManager = Depends(deps.get_run_manager), -) -> None: - """Establishes a WebSocket connection to communicate with an alive run process identified by its 'run_id'. - - The WebSocket URL should adhere to the format: /bot/run/connect?run_id=. - """ - - run_manager.logger.debug("Connecting to websocket") - run_id = websocket.query_params.get("run_id") - - # Validate run_id - if run_id is None: - run_manager.logger.error("No run_id provided") - raise WebSocketException(code=status.WS_1008_POLICY_VIOLATION) - if not run_id.isdigit(): - run_manager.logger.error("A non-digit run run_id provided") - raise WebSocketException(code=status.WS_1003_UNSUPPORTED_DATA) - run_id = int(run_id) - if run_id not in run_manager.processes: - run_manager.logger.error("process with run_id '%s' exited or never existed", run_id) - raise WebSocketException(code=status.WS_1014_BAD_GATEWAY) - - await websocket_manager.connect(websocket) - run_manager.logger.info("Websocket for run process '%s' has been opened", run_id) - - output_task = asyncio.create_task( - websocket_manager.send_process_output_to_websocket(run_id, run_manager, websocket) - ) - input_task = asyncio.create_task( - websocket_manager.forward_websocket_messages_to_process(run_id, run_manager, websocket) - ) - - # Wait for either task to finish - _, websocket_manager.pending_tasks[websocket] = await asyncio.wait( - [output_task, input_task], - return_when=asyncio.FIRST_COMPLETED, - ) - websocket_manager.disconnect(websocket) - if await run_manager.get_status(run_id) in [Status.ALIVE, Status.RUNNING]: - await run_manager.stop(run_id) +@router.post("/chat", status_code=201) +async def respond( + user_id: str, + user_message: str, +): + async with AsyncClient() as client: + try: + response = await client.post( + f"http://localhost:{settings.chatsky_port}/chat", + params={"user_id": user_id, "user_message": user_message}, + ) + return response.json() + except Exception as e: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=f"Please check that service's up and running on the port '{settings.chatsky_port}'.", + ) from e diff --git a/backend/chatsky_ui/api/api_v1/endpoints/chatsky_services.py b/backend/chatsky_ui/api/api_v1/endpoints/chatsky_services.py index 612a8e0d..d5cb6d62 100644 --- a/backend/chatsky_ui/api/api_v1/endpoints/chatsky_services.py +++ b/backend/chatsky_ui/api/api_v1/endpoints/chatsky_services.py @@ -1,32 +1,36 @@ import re from io import StringIO -from typing import Dict, Optional, Union +from typing import Dict, Union import aiofiles -from fastapi import APIRouter, Depends +from fastapi import APIRouter from pylint.lint import Run, pylinter from pylint.reporters.text import TextReporter -from chatsky_ui.api.deps import get_index from chatsky_ui.clients.chatsky_client import get_chatsky_conditions from chatsky_ui.core.config import settings from chatsky_ui.schemas.code_snippet import CodeSnippet -from chatsky_ui.services.index import Index +from chatsky_ui.services.json_converter.logic_component_converter.service_replacer import get_all_classes from chatsky_ui.utils.ast_utils import get_imports_from_file router = APIRouter() -@router.get("/search/{service_name}", status_code=200) -async def search_service(service_name: str, index: Index = Depends(get_index)) -> Dict[str, Optional[Union[str, list]]]: - """Searches for a custom service by name and returns its code. - - A service could be a condition, reponse, or pre/postservice. - """ - response = await index.search_service(service_name) +@router.get("/search/condition/{condition_name}", status_code=200) +async def search_condition(condition_name: str) -> Dict[str, Union[str, list]]: + """Searches for a custom condition by name and returns its code.""" + custom_classes = get_all_classes(settings.conditions_path) + response = [custom_class["body"] for custom_class in custom_classes if custom_class["name"] == condition_name] return {"status": "ok", "data": response} +@router.get("/get_all_custom_conditions", status_code=200) +async def get_all_custom_conditions_names() -> Dict[str, Union[str, list]]: + all_classes = get_all_classes(settings.conditions_path) + custom_classes = [custom_class["body"] for custom_class in all_classes] + return {"status": "ok", "data": custom_classes} + + @router.post("/lint_snippet", status_code=200) async def lint_snippet(snippet: CodeSnippet) -> Dict[str, str]: """Lints a snippet with Pylint. diff --git a/backend/chatsky_ui/api/api_v1/endpoints/flows.py b/backend/chatsky_ui/api/api_v1/endpoints/flows.py index 8ab047b0..f54e7d01 100644 --- a/backend/chatsky_ui/api/api_v1/endpoints/flows.py +++ b/backend/chatsky_ui/api/api_v1/endpoints/flows.py @@ -1,17 +1,42 @@ -from typing import Dict, Union +from pathlib import Path +from typing import Dict, Optional, Union -from fastapi import APIRouter +from dotenv import set_key +from fastapi import APIRouter, HTTPException, status +from git.exc import GitCommandError from omegaconf import OmegaConf from chatsky_ui.core.config import settings +from chatsky_ui.core.logger_config import get_logger from chatsky_ui.db.base import read_conf, write_conf +from chatsky_ui.utils.git_cmd import commit_changes, get_repo router = APIRouter() @router.get("/") -async def flows_get() -> Dict[str, Union[str, Dict[str, Union[list, dict]]]]: +async def flows_get(build_id: Optional[int] = None) -> Dict[str, Union[str, Dict[str, Union[list, dict]]]]: """Get the flows by reading the frontend_flows.yaml file.""" + repo = get_repo(settings.frontend_flows_path.parent) + + if build_id is not None: + tag = int(build_id) + try: + repo.git.checkout(tag, settings.frontend_flows_path.name) + except GitCommandError as e: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Build_id {tag} not found", + ) from e + else: + try: + repo.git.checkout("HEAD", settings.frontend_flows_path.name) + except GitCommandError as e: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Failed to checkout the latest commit", + ) from e + omega_flows = await read_conf(settings.frontend_flows_path) dict_flows = OmegaConf.to_container(omega_flows, resolve=True) return {"status": "ok", "data": dict_flows} # type: ignore @@ -20,5 +45,24 @@ async def flows_get() -> Dict[str, Union[str, Dict[str, Union[list, dict]]]]: @router.post("/") async def flows_post(flows: Dict[str, Union[list, dict]]) -> Dict[str, str]: """Write the flows to the frontend_flows.yaml file.""" + logger = get_logger(__name__) + repo = get_repo(settings.frontend_flows_path.parent) + + tags = sorted(repo.tags, key=lambda t: t.commit.committed_datetime) + repo.git.checkout(tags[-1], settings.frontend_flows_path.name) + await write_conf(flows, settings.frontend_flows_path) + logger.info("Flows saved to DB") + + commit_changes(repo, "Save frontend flows") + return {"status": "ok"} + + +@router.post("/tg_token") +async def post_tg_token(token: str): + dotenv_path = Path(settings.work_directory) / ".env" + dotenv_path.touch(exist_ok=True) + + set_key(dotenv_path, "TG_BOT_TOKEN", token) + return {"status": "ok", "message": "Token saved successfully"} diff --git a/backend/chatsky_ui/api/deps.py b/backend/chatsky_ui/api/deps.py index 7317d638..a63efa9f 100644 --- a/backend/chatsky_ui/api/deps.py +++ b/backend/chatsky_ui/api/deps.py @@ -1,7 +1,4 @@ -from chatsky_ui.core.config import settings -from chatsky_ui.services.index import Index from chatsky_ui.services.process_manager import BuildManager, RunManager -from chatsky_ui.services.websocket_manager import WebSocketManager build_manager = BuildManager() @@ -17,20 +14,3 @@ def get_build_manager() -> BuildManager: def get_run_manager() -> RunManager: run_manager.set_logger() return run_manager - - -websocket_manager = WebSocketManager() - - -def get_websocket_manager() -> WebSocketManager: - websocket_manager.set_logger() - return websocket_manager - - -index = Index() - - -def get_index() -> Index: - index.set_logger() - index.set_path(settings.index_path) - return index diff --git a/backend/chatsky_ui/cli.py b/backend/chatsky_ui/cli.py index 783cd093..1132118c 100644 --- a/backend/chatsky_ui/cli.py +++ b/backend/chatsky_ui/cli.py @@ -4,18 +4,20 @@ import string import sys from pathlib import Path +from typing import Optional import nest_asyncio import typer from cookiecutter.main import cookiecutter +from git import Repo from typing_extensions import Annotated -import yaml # Patch nest_asyncio before importing Chatsky nest_asyncio.apply = lambda: None from chatsky_ui.core.config import app_runner, settings # noqa: E402 from chatsky_ui.core.logger_config import get_logger # noqa: E402 +from chatsky_ui.utils.git_cmd import commit_changes # noqa: E402 cli = typer.Typer( help="🚀 Welcome to Chatsky-UI!\n\n" @@ -25,6 +27,15 @@ ) +def init_new_repo(git_path: Path, tag_name: str): + repo = Repo.init(git_path) + repo.git.checkout(b="dev") + commit_changes(repo, "Init frontend flows") + repo.create_tag(tag_name) + + print("Repo initialized with tag %s", tag_name) + + async def _execute_command(command_to_run): logger = get_logger(__name__) try: @@ -47,7 +58,7 @@ async def _execute_command(command_to_run): sys.exit(1) -def _execute_command_file(build_id: int, project_dir: Path, command_file: str, preset: str): +def _execute_command_file(project_dir: Path, command_file: str, preset: str, build_id: Optional[int] = None): logger = get_logger(__name__) presets_build_path = settings.presets / command_file @@ -69,7 +80,6 @@ def _execute_command_file(build_id: int, project_dir: Path, command_file: str, p @cli.command("build_bot") def build_bot( - build_id: Annotated[int, typer.Option(help="Id to save the build with")] = None, project_dir: Path = None, preset: Annotated[str, typer.Option(help="Could be one of: success, failure, loop")] = "success", ): @@ -80,12 +90,11 @@ def build_bot( raise NotADirectoryError(f"Directory {project_dir} doesn't exist") settings.set_config(work_directory=project_dir) - _execute_command_file(build_id, project_dir, "build.json", preset) + _execute_command_file(project_dir, "build.json", preset) @cli.command("build_scenario") def build_scenario( - build_id: Annotated[int, typer.Argument(help="Id to save the build with")], project_dir: Annotated[Path, typer.Option(help="Your Chatsky-UI project directory")] = ".", # TODO: add custom_dir - maybe the same way like project_dir ): @@ -94,12 +103,12 @@ def build_scenario( raise NotADirectoryError(f"Directory {project_dir} doesn't exist") settings.set_config(work_directory=project_dir) - from chatsky_ui.services.json_converter_new2.pipeline_converter import PipelineConverter # pylint: disable=C0415 + from chatsky_ui.services.json_converter.pipeline_converter import PipelineConverter # pylint: disable=C0415 - pipeline_converter = PipelineConverter(pipeline_id=build_id) + pipeline_converter = PipelineConverter() pipeline_converter( input_file=settings.frontend_flows_path, output_dir=settings.scripts_dir - ) #TODO: rename to frontend_graph_path + ) # TODO: rename to frontend_graph_path @cli.command("run_bot") @@ -115,7 +124,7 @@ def run_bot( raise NotADirectoryError(f"Directory {project_dir} doesn't exist") settings.set_config(work_directory=project_dir) - _execute_command_file(build_id, project_dir, "run.json", preset) + _execute_command_file(project_dir, "run.json", preset, build_id) @cli.command("run_scenario") @@ -124,13 +133,21 @@ def run_scenario( project_dir: Annotated[Path, typer.Option(help="Your Chatsky-UI project directory")] = ".", ): """Runs the bot with preset `success`""" + # checkout the commit and then run the build + bot_repo = Repo.init(Path(project_dir) / "bot") + bot_repo.git.checkout(build_id, "scripts/build.yaml") + if not project_dir.is_dir(): raise NotADirectoryError(f"Directory {project_dir} doesn't exist") settings.set_config(work_directory=project_dir) - script_path = settings.scripts_dir / f"build_{build_id}.yaml" + script_path = settings.scripts_dir / "build.yaml" command_to_run = f"python {project_dir}/app.py --script-path {script_path}" - asyncio.run(_execute_command(command_to_run)) + try: + asyncio.run(_execute_command(command_to_run)) + except FileNotFoundError: + command_to_run = f"python3 {project_dir}/app.py --script-path {script_path}" + asyncio.run(_execute_command(command_to_run)) @cli.command("run_app") @@ -179,10 +196,13 @@ def init( original_dir = os.getcwd() try: os.chdir(destination) - cookiecutter( - "https://github.com/Ramimashkouk/df_d_template.git", + proj_path = cookiecutter( + "https://github.com/deeppavlov/chatsky-ui-template.git", no_input=no_input, overwrite_if_exists=overwrite_if_exists, ) finally: os.chdir(original_dir) + + init_new_repo(Path(proj_path) / "bot", tag_name="0") + init_new_repo(Path(proj_path) / "chatsky_ui/app_data", tag_name="0") diff --git a/backend/chatsky_ui/core/config.py b/backend/chatsky_ui/core/config.py index 30cd10a9..a7450f2b 100644 --- a/backend/chatsky_ui/core/config.py +++ b/backend/chatsky_ui/core/config.py @@ -1,10 +1,21 @@ +import logging import os from pathlib import Path +from typing import Dict import uvicorn from dotenv import load_dotenv from omegaconf import DictConfig, OmegaConf +LOG_LEVELS: Dict[str, int] = { + "critical": logging.CRITICAL, + "error": logging.ERROR, + "warning": logging.WARNING, + "info": logging.INFO, + "debug": logging.DEBUG, +} + +logging.basicConfig(level=LOG_LEVELS[os.getenv("LOG_LEVEL", "info")]) load_dotenv() @@ -22,6 +33,7 @@ def __init__(self): self.set_config( host=os.getenv("HOST", "0.0.0.0"), port=os.getenv("PORT", "8000"), + chatsky_port=os.getenv("CHATSKY_PORT", "8020"), log_level=os.getenv("LOG_LEVEL", "info"), conf_reload=os.getenv("CONF_RELOAD", "false"), work_directory=".", @@ -33,11 +45,12 @@ def set_config(self, **kwargs): value = Path(value) elif key == "conf_reload": value = str(value).lower() in ["true", "yes", "t", "y", "1"] - elif key == "port": + elif key in ["port", "CHATSKY_PORT"]: value = int(value) setattr(self, key, value) if "work_directory" in kwargs: + logging.debug("Setting work directory to %s", self.work_directory) self._set_user_proj_paths() def _set_user_proj_paths(self): @@ -49,7 +62,6 @@ def _set_user_proj_paths(self): self.snippet2lint_path = self.work_directory / "chatsky_ui/.snippet2lint.py" self.custom_dir = self.work_directory / "bot/custom" - self.index_path = self.custom_dir / ".services_index.yaml" self.conditions_path = self.custom_dir / "conditions.py" self.responses_path = self.custom_dir / "responses.py" self.scripts_dir = self.work_directory / "bot/scripts" @@ -63,6 +75,7 @@ def save_config(self): "work_directory": str(self.work_directory), "host": self.host, "port": self.port, + "chatsky_port": self.chatsky_port, "log_level": self.log_level, "conf_reload": self.conf_reload, } diff --git a/backend/chatsky_ui/core/logger_config.py b/backend/chatsky_ui/core/logger_config.py index 8fd0f345..9ae891cf 100644 --- a/backend/chatsky_ui/core/logger_config.py +++ b/backend/chatsky_ui/core/logger_config.py @@ -1,17 +1,9 @@ import logging from datetime import datetime from pathlib import Path -from typing import Dict, Literal, Optional +from typing import Literal, Optional -from chatsky_ui.core.config import settings - -LOG_LEVELS: Dict[str, int] = { - "critical": logging.CRITICAL, - "error": logging.ERROR, - "warning": logging.WARNING, - "info": logging.INFO, - "debug": logging.DEBUG, -} +from chatsky_ui.core.config import LOG_LEVELS, settings def setup_logging(log_type: Literal["builds", "runs"], id_: int, timestamp: datetime) -> Path: diff --git a/backend/chatsky_ui/main.py b/backend/chatsky_ui/main.py index 8bb10362..5de32bd9 100644 --- a/backend/chatsky_ui/main.py +++ b/backend/chatsky_ui/main.py @@ -1,3 +1,4 @@ +import signal from contextlib import asynccontextmanager from fastapi import APIRouter, FastAPI, Response @@ -6,22 +7,26 @@ from chatsky_ui import __version__ from chatsky_ui.api.api_v1.api import api_router -from chatsky_ui.api.deps import get_index +from chatsky_ui.api.deps import run_manager from chatsky_ui.core.config import settings -index_dict = {} + +def signal_handler(self, signum): + global stop_background_task + print("Caught termination signal, shutting down gracefully...") + for process in run_manager.processes.values(): + process.to_be_terminated = True @asynccontextmanager async def lifespan(app: FastAPI): if settings.temp_conf.exists(): settings.refresh_work_dir() - - index_dict["instance"] = get_index() - await index_dict["instance"].load() + signal.signal(signal.SIGINT, signal_handler) yield - # settings.temp_conf.unlink(missing_ok=True) + settings.temp_conf.unlink(missing_ok=True) + await run_manager.stop_all() app = FastAPI(title="DF Designer", version=__version__, lifespan=lifespan) diff --git a/backend/chatsky_ui/schemas/front_graph_components/base_component.py b/backend/chatsky_ui/schemas/front_graph_components/base_component.py index 6cf3aa88..4fd6aba2 100644 --- a/backend/chatsky_ui/schemas/front_graph_components/base_component.py +++ b/backend/chatsky_ui/schemas/front_graph_components/base_component.py @@ -1,4 +1,5 @@ from pydantic import BaseModel + class BaseComponent(BaseModel): pass diff --git a/backend/chatsky_ui/schemas/front_graph_components/info_holders/condition.py b/backend/chatsky_ui/schemas/front_graph_components/info_holders/condition.py index 3789472f..199c4ee6 100644 --- a/backend/chatsky_ui/schemas/front_graph_components/info_holders/condition.py +++ b/backend/chatsky_ui/schemas/front_graph_components/info_holders/condition.py @@ -10,4 +10,4 @@ class CustomCondition(Condition): class SlotCondition(Condition): - slot_id: str # not the condition id + slot_id: str # not the condition id diff --git a/backend/chatsky_ui/schemas/front_graph_components/interface.py b/backend/chatsky_ui/schemas/front_graph_components/interface.py index c5bafbfb..e21c1d15 100644 --- a/backend/chatsky_ui/schemas/front_graph_components/interface.py +++ b/backend/chatsky_ui/schemas/front_graph_components/interface.py @@ -1,22 +1,32 @@ +import os +from typing import Any, Dict, Optional + +from dotenv import load_dotenv from pydantic import Field, model_validator -from typing import Any + +from chatsky_ui.core.config import settings from .base_component import BaseComponent -from typing import Optional, Dict + +load_dotenv(os.path.join(settings.work_directory, ".env"), override=True) + class Interface(BaseComponent): + model_config = {"extra": "forbid"} + telegram: Optional[Dict[str, Any]] = Field(default=None) - cli: Optional[Dict[str, Any]] = Field(default=None) + http: Optional[Dict[str, Any]] = Field(default=None) - @model_validator(mode='after') + @model_validator(mode="after") def check_one_not_none(cls, values): - telegram, cli = values.telegram, values.cli - if (telegram is None) == (cli is None): - raise ValueError('Exactly one of "telegram" or "cli" must be provided.') + non_none_values = [x for x in [values.telegram, values.http] if x is not None] + if len(non_none_values) != 1: + raise ValueError('Exactly one of "telegram", or "http" must be provided.') return values - - @model_validator(mode='after') + + @model_validator(mode="after") def check_telegram_token(cls, values): - if values.telegram is not None and 'token' not in values.telegram: - raise ValueError('Telegram token must be provided.') + tg_bot_token = os.getenv("TG_BOT_TOKEN") + if values.telegram is not None and not tg_bot_token: + raise ValueError("Telegram token must be provided.") return values diff --git a/backend/chatsky_ui/schemas/front_graph_components/node.py b/backend/chatsky_ui/schemas/front_graph_components/node.py index c47e6198..1f9e56e3 100644 --- a/backend/chatsky_ui/schemas/front_graph_components/node.py +++ b/backend/chatsky_ui/schemas/front_graph_components/node.py @@ -1,5 +1,7 @@ from typing import List +from pydantic import model_validator + from .base_component import BaseComponent @@ -20,3 +22,10 @@ class LinkNode(Node): class SlotsNode(Node): groups: List[dict] + + @model_validator(mode="after") + def check_unique_groups_names(cls, values) -> "SlotsNode": + groups_names = [group["name"] for group in values.groups] + if len(groups_names) != len(set(groups_names)): + raise ValueError(f"Slot groups names should be unique. Got duplicates: {groups_names}") + return values diff --git a/backend/chatsky_ui/schemas/front_graph_components/slot.py b/backend/chatsky_ui/schemas/front_graph_components/slot.py index 1ef338d1..1695286b 100644 --- a/backend/chatsky_ui/schemas/front_graph_components/slot.py +++ b/backend/chatsky_ui/schemas/front_graph_components/slot.py @@ -1,7 +1,8 @@ -from typing import Optional, List +from typing import List from .base_component import BaseComponent + class Slot(BaseComponent): name: str diff --git a/backend/chatsky_ui/services/index.py b/backend/chatsky_ui/services/index.py deleted file mode 100644 index 05427c41..00000000 --- a/backend/chatsky_ui/services/index.py +++ /dev/null @@ -1,185 +0,0 @@ -""" -Index service -------------- - -The Index service is responsible for indexing the user bot's conditions, responses, and services. -By indexing the project, the Index service creates an in-memory representation that can be -quickly accessed when needed. -""" -import asyncio -from pathlib import Path -from typing import Dict, List, Optional - -from omegaconf import OmegaConf -from omegaconf.dictconfig import DictConfig - -from chatsky_ui.core.config import settings -from chatsky_ui.core.logger_config import get_logger -from chatsky_ui.db.base import read_conf, read_logs, write_conf - - -class Index: - def __init__(self): - self.index: dict = {} - self.conditions: List[str] = [] - self.responses: List[str] = [] - self.services: List[str] = [] - self._logger = None - self._path = None - - @property - def logger(self): - if self._logger is None: - raise ValueError("Logger has not been configured. Call set_logger() first.") - return self._logger - - def set_logger(self): - self._logger = get_logger(__name__) - - @property - def path(self): - if self._path is None: - raise ValueError("Path has not been configured. Call set_path() first.") - return self._path - - def set_path(self, path: Path): - self._path = path - - async def _load_index(self) -> None: - """Load indexed conditions, responses and services from disk.""" - db_index: DictConfig = await read_conf(self.path) # type: ignore - index_dict: Dict[str, dict] = OmegaConf.to_container(db_index, resolve=True) # type: ignore - self.index = index_dict - self.logger.debug("Index loaded") - - async def _load_conditions(self) -> None: - """Load conditions from disk.""" - path = settings.conditions_path - if path.exists(): - self.conditions = await read_logs(path) - self.logger.debug("Conditions loaded") - else: - self.logger.warning("No conditions file found") - - async def _load_responses(self) -> None: - """Load responses from disk.""" - path = settings.responses_path - if path.exists(): - self.responses = await read_logs(path) - self.logger.debug("Responses loaded") - else: - self.logger.warning("No responses file found") - - async def _load_services(self) -> None: - """Load services from disk.""" - path = settings.responses_path.parent / "services.py" - if path.exists(): - self.services = await read_logs(path) - self.logger.debug("Services loaded") - else: - self.logger.warning("No services file found") - - def _get_service_code(self, services_lst: List[str], lineno: int) -> List[str]: - """Get service code from services list. - - Example: - >>> _get_service_code(["def is_upper_case(name):\n", " return name.isupper()"], 1) - ['def is_upper_case(name):\n', ' return name.isupper()'] - """ - service: List[str] = [] - func_lines: List[str] = services_lst[lineno - 1 :] - self.logger.debug("services_lst: %s", services_lst) - for func_lineno, func_line in enumerate(func_lines): - if func_line.startswith("def ") and func_lineno != 0: - break - service.append(func_line) # ?maybe with \n - return service - - async def load(self) -> None: - """Load index and services into memory.""" - if not self.path.exists(): - raise FileNotFoundError(f"File {self.path} doesn't exist") - - await asyncio.gather( - self._load_index(), - self._load_conditions(), - self._load_responses(), - self._load_services(), - ) - self.logger.info("Index and services loaded") - self.logger.debug("Loaded index: %s", self.index) - - def get_services(self) -> dict: - """Get indexed services. - - Example: - >>> get_services() - { - "is_upper_case": {"type": "condition", "lineno": 3}, - "say_hi": {"type": "response", "lineno": 5} - } - """ - return self.index - - async def search_service(self, service_name: str) -> Optional[List[str]]: - """Get the body code of a service based on its indexed info (type, lineno). - - Example: - >>> search_service("is_upper_case") - ["def is_upper_case(name):\n", " return name.isupper()"] - - """ - if service_name not in self.index: - return [] - service_type = self.index[service_name]["type"] - lineno = int(self.index[service_name]["lineno"]) - - if service_type == "condition": - return self._get_service_code(self.conditions, lineno) - elif service_type == "response": - return self._get_service_code(self.responses, lineno) - elif service_type == "service": - return self._get_service_code(self.services, lineno) - - async def indexit(self, service_name: str, type_: str, lineno: int) -> None: - """Add service info to the index using indexit_all method.""" - self.logger.debug("Indexing '%s'", service_name) - await self.indexit_all([service_name], [type_], [lineno]) - self.logger.info("Indexed '%s'", service_name) - - async def indexit_all(self, services_names: List[str], types: List[str], linenos: List[int]) -> None: - """Index multiple services. - - The index is added to the index in the form: {service_name: {"type": ``type_``, "lineno": lineno}}. - - Args: - services_names: list of service names - types: list of service types ("condition", "response", "service") - linenos: list of service starting line numbers according to its place in the file. - - Raises: - FileNotFoundError: if the index file doesn't exist - - Example: - >>> services_names = ["is_upper_case", "say_hi"] - >>> types = ["condition", "response"] - >>> linenos = [3, 5] - >>> await indexit_all(services_names, types, linenos) - { - "is_upper_case": {"type": "condition", "lineno": 3}, - "say_hi": {"type": "response", "lineno": 5} - } - - Returns: - None - """ - if not self.path.exists(): - raise FileNotFoundError(f"File {self.path} doesn't exist") - - for service_name, type_, lineno in zip(services_names, types, linenos): - self.index[service_name] = { - "type": type_, # condition/response/service - "lineno": lineno, - } - - await write_conf(self.index, self.path) # ?to background tasks diff --git a/backend/chatsky_ui/services/json_converter.py b/backend/chatsky_ui/services/json_converter.py deleted file mode 100644 index 73cc1824..00000000 --- a/backend/chatsky_ui/services/json_converter.py +++ /dev/null @@ -1,281 +0,0 @@ -""" -JSON Converter ---------------- - -Converts a user project's frontend graph to a script understandable by Chatsky json-importer. -""" -import ast -from collections import defaultdict -from pathlib import Path -from typing import List, Optional, Tuple - -from omegaconf.dictconfig import DictConfig - -from chatsky_ui.core.config import settings -from chatsky_ui.core.logger_config import get_logger -from chatsky_ui.db.base import read_conf, write_conf -from chatsky_ui.services.condition_finder import ServiceReplacer - -logger = get_logger(__name__) - -PRE_TRANSITIONS_PROCESSING = "PRE_TRANSITIONS_PROCESSING" - - -PRE_TRANSITION = "PRE_TRANSITION" - - -def _get_db_paths(build_id: int) -> Tuple[Path, Path, Path, Path]: - """Get paths to frontend graph, chatsky script, and chatsky custom conditions files.""" - frontend_graph_path = settings.frontend_flows_path - custom_conditions_file = settings.conditions_path - custom_responses_file = settings.responses_path - script_path = settings.scripts_dir / f"build_{build_id}.yaml" - - if not frontend_graph_path.exists(): - raise FileNotFoundError(f"File {frontend_graph_path} doesn't exist") - if not custom_conditions_file.exists(): - raise FileNotFoundError(f"File {custom_conditions_file} doesn't exist") - if not custom_responses_file.exists(): - raise FileNotFoundError(f"File {custom_responses_file} doesn't exist") - if not script_path.exists(): - script_path.parent.mkdir(parents=True, exist_ok=True) - script_path.touch() - - return frontend_graph_path, script_path, custom_conditions_file, custom_responses_file - - -def _organize_graph_according_to_nodes(flow_graph: DictConfig, script: dict) -> Tuple[dict, dict]: - nodes = {} - for flow in flow_graph["flows"]: - node_names_in_one_flow = [] - for node in flow.data.nodes: - if "flags" in node.data: - if "start" in node.data.flags: - if "start_label" in script: - raise ValueError("There are more than one start node in the script") - script["start_label"] = [flow.name, node.data.name] - if "fallback" in node.data.flags: - if "fallback_label" in script: - raise ValueError("There are more than one fallback node in the script") - script["fallback_label"] = [flow.name, node.data.name] - - if node.data.name in node_names_in_one_flow: - raise ValueError(f"There is more than one node with the name '{node.data.name}' in the same flow.") - node_names_in_one_flow.append(node.data.name) - nodes[node.id] = {"info": node} - nodes[node.id]["flow"] = flow.name - nodes[node.id]["TRANSITIONS"] = [] - nodes[node.id][PRE_TRANSITION] = dict() - - def _convert_slots(slots: dict) -> dict: - group_slot = defaultdict(dict) - for slot_name, slot_values in slots.copy().items(): - slot_type = slot_values["type"] - del slot_values["id"] - del slot_values["type"] - if slot_type != "GroupSlot": - group_slot[slot_name].update({f"chatsky.slots.{slot_type}": {k: v for k, v in slot_values.items()}}) - else: - group_slot[slot_name] = _convert_slots(slot_values) - return dict(group_slot) - - if "slots" in flow_graph: - script["slots"] = _convert_slots(flow_graph["slots"]) - - return nodes, script - - -def _get_condition(nodes: dict, edge: DictConfig) -> Optional[DictConfig]: - """Get node's condition from `nodes` according to `edge` info.""" - return next( - (condition for condition in nodes[edge.source]["info"].data.conditions if condition["id"] == edge.sourceHandle), - None, - ) - - -def _add_transitions(nodes: dict, edge: DictConfig, condition: DictConfig, slots: DictConfig) -> None: - """Add transitions to a node according to `edge` and `condition`.""" - - def _get_slot(slots, id_): - if not slots: - return "" - for name, value in slots.copy().items(): - slot_path = name - if value.get("id") == id_: - return name - elif value.get("type") != "GroupSlot": - continue - else: - del value["id"] - del value["type"] - slot_path = _get_slot(value, id_) - if slot_path: - slot_path = ".".join([name, slot_path]) - return slot_path - - if condition["type"] == "python": - converted_cnd = {f"custom.conditions.{condition.name}": None} - elif condition["type"] == "slot": - slot = _get_slot(slots, id_=condition.data.slot) - converted_cnd = {"chatsky.conditions.slots.SlotsExtracted": slot} - nodes[edge.source][PRE_TRANSITION].update({slot: {"chatsky.processing.slots.Extract": slot}}) - # TODO: elif condition["type"] == "chatsky": - else: - raise ValueError(f"Unknown condition type: {condition['type']}") - - # if the edge is a link_node, we add transition of its source and target - if nodes[edge.target]["info"].type == "link_node": - flow = nodes[edge.target]["info"].data.transition.target_flow - - target_node = nodes[edge.target]["info"].data.transition.target_node - node = nodes[target_node]["info"].data.name - else: - flow = nodes[edge.target]["flow"] - node = nodes[edge.target]["info"].data.name - - nodes[edge.source]["TRANSITIONS"].append( - { - "dst": [ - flow, - node, - ], - "priority": condition.data.priority, - "cnd": converted_cnd, - } - ) - - -def _fill_nodes_into_script(nodes: dict, script: dict) -> None: - """Fill nodes into chatsky script dictunary.""" - for _, node in nodes.items(): - if node["info"].type in ["link_node", "slots_node"]: - continue - if node["flow"] not in script["script"]: - script["script"][node["flow"]] = {} - script["script"][node["flow"]].update( - { - node["info"].data.name: { - "RESPONSE": node["info"].data.response, - "TRANSITIONS": node["TRANSITIONS"], - PRE_TRANSITION: node[PRE_TRANSITION], - } - } - ) - - -async def update_responses_lines(nodes: dict) -> Tuple[dict, List[str]]: - """Organizes the responses in nodes in a format that json-importer accepts. - - If the response type is "python", its function will be added to responses_lines to be written - to the custom_conditions_file later. - * If the response already exists in the responses_lines, it will be replaced with the new one. - """ - responses_list = [] - for node in nodes.values(): - if node["info"].type in ["link_node", "slots_node"]: - continue - response = node["info"].data.response - logger.debug("response type: %s", response.type) - if response.type == "python": - response.data = response.data[0] - logger.info("Adding response: %s", response) - - responses_list.append(response.data.python.action) - node["info"].data.response = {f"custom.responses.{response.name}": None} - elif response.type == "text": - response.data = response.data[0] - logger.debug("Adding response: %s", response.data.text) - node["info"].data.response = {"chatsky.Message": {"text": response.data.text}} - elif response.type == "choice": - # logger.debug("Adding response: %s", ) - chatsky_responses = [] - for message in response.data: - if "text" in message: - chatsky_responses.append({"chatsky.Message": {"text": message["text"]}}) - else: # TODO: check: are you sure that you can use only "text" type inside a choice? - raise ValueError("Unknown response type. There must be a 'text' field in each message.") - node["info"].data.response = {"chatsky.rsp.choice": chatsky_responses.copy()} - else: - raise ValueError(f"Unknown response type: {response.type}") - return nodes, responses_list - - -def map_interface(interface: DictConfig) -> dict: - """Map frontend interface to chatsky interface.""" - if not isinstance(interface, DictConfig): - raise ValueError(f"Interface must be a dictionary. Got: {type(interface)}") - keys = interface.keys() - if len(keys) != 1: - raise ValueError("There must be only one key in the interface") - - key = next(iter(keys)) - if key == "telegram": - if "token" not in interface[key]: - raise ValueError("Token keyworkd is not provided for telegram interface") - if not interface[key]["token"]: - raise ValueError("Token is not provided for telegram interface") - return {"chatsky.messengers.telegram.LongpollingInterface": {"token": interface[key]["token"]}} - if key == "cli": - return {"chatsky.messengers.console.CLIMessengerInterface": {}} - else: - raise ValueError(f"Unknown interface: {key}") - - -async def converter(build_id: int) -> None: - """Translate frontend flow script into chatsky script.""" - frontend_graph_path, script_path, custom_conditions_file, custom_responses_file = _get_db_paths(build_id) - - flow_graph: DictConfig = await read_conf(frontend_graph_path) # type: ignore - script = { - "script": {}, - "messenger_interface": map_interface(flow_graph["interface"]), - } - del flow_graph["interface"] - - nodes, script = _organize_graph_according_to_nodes(flow_graph, script) - - with open(custom_responses_file, "r", encoding="UTF-8") as file: - responses_tree = ast.parse(file.read()) - - nodes, responses_list = await update_responses_lines(nodes) - - logger.info("Responses list: %s", responses_list) - replacer = ServiceReplacer(responses_list) - replacer.visit(responses_tree) - - with open(custom_responses_file, "w") as file: - file.write(ast.unparse(responses_tree)) - - with open(custom_conditions_file, "r", encoding="UTF-8") as file: - conditions_tree = ast.parse(file.read()) - - conditions_list = [] - - for flow in flow_graph["flows"]: - for edge in flow.data.edges: - if edge.source in nodes and edge.target in nodes: - condition = _get_condition(nodes, edge) - if condition is None: - logger.error( - "A condition of edge '%s' - '%s' and id of '%s' is not found in the corresponding node", - edge.source, - edge.target, - edge.sourceHandle, - ) - continue - if condition.type == "python": - conditions_list.append(condition.data.python.action) - - _add_transitions(nodes, edge, condition, flow_graph["slots"]) - else: - logger.error("A node of edge '%s-%s' is not found in nodes", edge.source, edge.target) - - replacer = ServiceReplacer(conditions_list) - replacer.visit(conditions_tree) - - with open(custom_conditions_file, "w") as file: - file.write(ast.unparse(conditions_tree)) - - _fill_nodes_into_script(nodes, script) - - await write_conf(script, script_path) diff --git a/backend/chatsky_ui/services/json_converter_new2/base_converter.py b/backend/chatsky_ui/services/json_converter/base_converter.py similarity index 99% rename from backend/chatsky_ui/services/json_converter_new2/base_converter.py rename to backend/chatsky_ui/services/json_converter/base_converter.py index 6d654f12..aa5dff17 100644 --- a/backend/chatsky_ui/services/json_converter_new2/base_converter.py +++ b/backend/chatsky_ui/services/json_converter/base_converter.py @@ -1,5 +1,6 @@ from abc import ABC, abstractmethod + class BaseConverter(ABC): def __call__(self, *args, **kwargs): return self._convert() diff --git a/backend/chatsky_ui/services/json_converter/consts.py b/backend/chatsky_ui/services/json_converter/consts.py new file mode 100644 index 00000000..388bc405 --- /dev/null +++ b/backend/chatsky_ui/services/json_converter/consts.py @@ -0,0 +1,3 @@ +RESPONSES_FILE = "responses" +CONDITIONS_FILE = "conditions" +CUSTOM_FILE = "custom" diff --git a/backend/chatsky_ui/services/json_converter_new2/flow_converter.py b/backend/chatsky_ui/services/json_converter/flow_converter.py similarity index 84% rename from backend/chatsky_ui/services/json_converter_new2/flow_converter.py rename to backend/chatsky_ui/services/json_converter/flow_converter.py index 3dc3ac6e..ddbb1aae 100644 --- a/backend/chatsky_ui/services/json_converter_new2/flow_converter.py +++ b/backend/chatsky_ui/services/json_converter/flow_converter.py @@ -1,7 +1,8 @@ -from typing import Dict, List, Any, Tuple +from typing import Any, Dict, List, Tuple + from ...schemas.front_graph_components.flow import Flow -from .node_converter import InfoNodeConverter, LinkNodeConverter from .base_converter import BaseConverter +from .node_converter import InfoNodeConverter, LinkNodeConverter class FlowConverter(BaseConverter): @@ -23,13 +24,15 @@ def __call__(self, *args, **kwargs): self.slots_conf = kwargs["slots_conf"] self._integrate_edges_into_nodes() return super().__call__(*args, **kwargs) - + def _validate_flow(self, flow: Dict[str, Any]): if "data" not in flow or "nodes" not in flow["data"] or "edges" not in flow["data"]: raise ValueError("Invalid flow structure") def _integrate_edges_into_nodes(self): - def _insert_dst_into_condition(node: Dict[str, Any], condition_id: str, target_node: Tuple[str, str]) -> Dict[str, Any]: + def _insert_dst_into_condition( + node: Dict[str, Any], condition_id: str, target_node: Tuple[str, str] + ) -> Dict[str, Any]: for condition in node["data"]["conditions"]: if condition["id"] == condition_id: condition["dst"] = target_node @@ -46,7 +49,7 @@ def _insert_dst_into_condition(node: Dict[str, Any], condition_id: str, target_n def _map_edges(self) -> List[Dict[str, Any]]: def _get_flow_and_node_names(target_node): node_type = target_node["type"] - if node_type == "link_node": #TODO: WHY CONVERTING HERE? + if node_type == "link_node": # TODO: WHY CONVERTING HERE? return LinkNodeConverter(target_node)(mapped_flows=self.mapped_flows) elif node_type == "default_node": return [self.flow.name, target_node["data"]["name"]] @@ -54,7 +57,6 @@ def _get_flow_and_node_names(target_node): edges = self.flow.edges.copy() for edge in edges: target_id = edge["target"] - # target_node = _find_node_by_id(target_id, self.flow.nodes) target_node = self.mapped_flows[self.flow.name].get(target_id) if target_node: edge["target"] = _get_flow_and_node_names(target_node) @@ -64,5 +66,7 @@ def _convert(self) -> Dict[str, Any]: converted_flow = {self.flow.name: {}} for node in self.flow.nodes: if node["type"] == "default_node": - converted_flow[self.flow.name].update({node["data"]["name"]: InfoNodeConverter(node)(slots_conf=self.slots_conf)}) + converted_flow[self.flow.name].update( + {node["data"]["name"]: InfoNodeConverter(node)(slots_conf=self.slots_conf)} + ) return converted_flow diff --git a/backend/chatsky_ui/services/json_converter_new2/interface_converter.py b/backend/chatsky_ui/services/json_converter/interface_converter.py similarity index 51% rename from backend/chatsky_ui/services/json_converter_new2/interface_converter.py rename to backend/chatsky_ui/services/json_converter/interface_converter.py index 42ba348a..380a6dcc 100644 --- a/backend/chatsky_ui/services/json_converter_new2/interface_converter.py +++ b/backend/chatsky_ui/services/json_converter/interface_converter.py @@ -1,14 +1,15 @@ -from .base_converter import BaseConverter +from chatsky_ui.core.config import settings + from ...schemas.front_graph_components.interface import Interface +from .base_converter import BaseConverter + class InterfaceConverter(BaseConverter): def __init__(self, interface: dict): self.interface = Interface(**interface) def _convert(self): - if self.interface.cli is not None: - return {"chatsky.messengers.console.CLIMessengerInterface": {}} + if self.interface.http is not None: + return {"chatsky.messengers.HTTPMessengerInterface": {"port": settings.chatsky_port}} elif self.interface.telegram is not None: - return { - "chatsky.messengers.telegram.LongpollingInterface": {"token": self.interface.telegram["token"]} - } + return {"chatsky.messengers.TelegramInterface": {"token": {"external:os.getenv": "TG_BOT_TOKEN"}}} diff --git a/backend/chatsky_ui/services/json_converter_new2/logic_component_converter/condition_converter.py b/backend/chatsky_ui/services/json_converter/logic_component_converter/condition_converter.py similarity index 52% rename from backend/chatsky_ui/services/json_converter_new2/logic_component_converter/condition_converter.py rename to backend/chatsky_ui/services/json_converter/logic_component_converter/condition_converter.py index 0c25737f..a59ce3db 100644 --- a/backend/chatsky_ui/services/json_converter_new2/logic_component_converter/condition_converter.py +++ b/backend/chatsky_ui/services/json_converter/logic_component_converter/condition_converter.py @@ -1,13 +1,16 @@ from abc import ABC, abstractmethod -import ast -from ..consts import CUSTOM_FILE, CONDITIONS_FILE -from ..base_converter import BaseConverter -from ....schemas.front_graph_components.info_holders.condition import CustomCondition, SlotCondition from ....core.config import settings +from ....schemas.front_graph_components.info_holders.condition import CustomCondition, SlotCondition +from ..base_converter import BaseConverter +from ..consts import CONDITIONS_FILE, CUSTOM_FILE from .service_replacer import store_custom_service +class BadConditionException(Exception): + pass + + class ConditionConverter(BaseConverter, ABC): @abstractmethod def get_pre_transitions(): @@ -16,28 +19,31 @@ def get_pre_transitions(): class CustomConditionConverter(ConditionConverter): def __init__(self, condition: dict): - self.condition = CustomCondition( - name=condition["name"], - code=condition["data"]["python"]["action"], - ) + self.condition = None + try: + self.condition = CustomCondition( + name=condition["name"], + code=condition["data"]["python"]["action"], + ) + except KeyError as missing_key: + raise BadConditionException("Missing key in custom condition data") from missing_key def _convert(self): store_custom_service(settings.conditions_path, [self.condition.code]) - custom_cnd = { - f"{CUSTOM_FILE}.{CONDITIONS_FILE}.{self.condition.name}": None - } + custom_cnd = {f"{CUSTOM_FILE}.{CONDITIONS_FILE}.{self.condition.name}": None} return custom_cnd - + def get_pre_transitions(self): return {} class SlotConditionConverter(ConditionConverter): def __init__(self, condition: dict): - self.condition = SlotCondition( - slot_id=condition["data"]["slot"], - name=condition["name"] - ) + self.condition = None + try: + self.condition = SlotCondition(slot_id=condition["data"]["slot"], name=condition["name"]) + except KeyError as missing_key: + raise BadConditionException("Missing key in slot condition data") from missing_key def __call__(self, *args, **kwargs): self.slots_conf = kwargs["slots_conf"] @@ -47,9 +53,5 @@ def _convert(self): return {"chatsky.conditions.slots.SlotsExtracted": self.slots_conf[self.condition.slot_id]} def get_pre_transitions(self): - slot_path = self.slots_conf[self.condition.slot_id] - return { - slot_path: { - "chatsky.processing.slots.Extract": slot_path - } - } + slot_path = self.slots_conf[self.condition.slot_id] # type: ignore + return {slot_path: {"chatsky.processing.slots.Extract": slot_path}} diff --git a/backend/chatsky_ui/services/json_converter/logic_component_converter/response_converter.py b/backend/chatsky_ui/services/json_converter/logic_component_converter/response_converter.py new file mode 100644 index 00000000..cb2624ee --- /dev/null +++ b/backend/chatsky_ui/services/json_converter/logic_component_converter/response_converter.py @@ -0,0 +1,42 @@ +from ....core.config import settings +from ....schemas.front_graph_components.info_holders.response import CustomResponse, TextResponse +from ..base_converter import BaseConverter +from ..consts import CUSTOM_FILE, RESPONSES_FILE +from .service_replacer import store_custom_service + + +class BadResponseException(Exception): + pass + + +class ResponseConverter(BaseConverter): + pass + + +class TextResponseConverter(ResponseConverter): + def __init__(self, response: dict): + try: + self.response = TextResponse( + name=response["name"], + text=next(iter(response["data"]))["text"], + ) + except KeyError as e: + raise BadResponseException("Missing key in custom condition data") from e + + def _convert(self): + return {"chatsky.Message": {"text": self.response.text}} + + +class CustomResponseConverter(ResponseConverter): + def __init__(self, response: dict): + try: + self.response = CustomResponse( + name=response["name"], + code=next(iter(response["data"]))["python"]["action"], + ) + except KeyError as e: + raise BadResponseException("Missing key in custom response data") from e + + def _convert(self): + store_custom_service(settings.responses_path, [self.response.code]) + return {f"{CUSTOM_FILE}.{RESPONSES_FILE}.{self.response.name}": None} diff --git a/backend/chatsky_ui/services/json_converter_new2/logic_component_converter/service_replacer.py b/backend/chatsky_ui/services/json_converter/logic_component_converter/service_replacer.py similarity index 66% rename from backend/chatsky_ui/services/json_converter_new2/logic_component_converter/service_replacer.py rename to backend/chatsky_ui/services/json_converter/logic_component_converter/service_replacer.py index 4f601fc9..6dee45c1 100644 --- a/backend/chatsky_ui/services/json_converter_new2/logic_component_converter/service_replacer.py +++ b/backend/chatsky_ui/services/json_converter/logic_component_converter/service_replacer.py @@ -1,16 +1,24 @@ import ast from ast import NodeTransformer -from typing import Dict, List from pathlib import Path +from typing import Dict, List from chatsky_ui.core.logger_config import get_logger -logger = get_logger(__name__) - class ServiceReplacer(NodeTransformer): def __init__(self, new_services: List[str]): self.new_services_classes = self._get_classes_def(new_services) + self._logger = None + + @property + def logger(self): + if self._logger is None: + raise ValueError("Logger has not been configured. Call set_logger() first.") + return self._logger + + def set_logger(self): + self._logger = get_logger(__name__) def _get_classes_def(self, services_code: List[str]) -> Dict[str, ast.ClassDef]: parsed_codes = [ast.parse(service_code) for service_code in services_code] @@ -24,11 +32,11 @@ def _extract_class_defs(self, parsed_code: ast.Module, service_code: str): if isinstance(node, ast.ClassDef): classes[node.name] = node else: - logger.error("No class definition found in new_service: %s", service_code) + self.logger.error("No class definition found in new_service: %s", service_code) return classes def visit_ClassDef(self, node: ast.ClassDef) -> ast.ClassDef: - logger.debug("Visiting class '%s' and comparing with: %s", node.name, self.new_services_classes.keys()) + self.logger.debug("Visiting class '%s' and comparing with: %s", node.name, self.new_services_classes.keys()) if node.name in self.new_services_classes: return self._get_class_def(node) return node @@ -36,6 +44,7 @@ def visit_ClassDef(self, node: ast.ClassDef) -> ast.ClassDef: def _get_class_def(self, node: ast.ClassDef) -> ast.ClassDef: service = self.new_services_classes[node.name] del self.new_services_classes[node.name] + self.logger.info("Updating class '%s'", node.name) return service def generic_visit(self, node: ast.AST): @@ -45,7 +54,7 @@ def generic_visit(self, node: ast.AST): return node def _append_new_services(self, node: ast.Module): - logger.info("Services not found, appending new services: %s", list(self.new_services_classes.keys())) + self.logger.info("Services not found, appending new services: %s", list(self.new_services_classes.keys())) for _, service in self.new_services_classes.items(): node.body.append(service) @@ -53,9 +62,21 @@ def _append_new_services(self, node: ast.Module): def store_custom_service(services_path: Path, services: List[str]): with open(services_path, "r", encoding="UTF-8") as file: conditions_tree = ast.parse(file.read()) - + replacer = ServiceReplacer(services) + replacer.set_logger() replacer.visit(conditions_tree) with open(services_path, "w") as file: file.write(ast.unparse(conditions_tree)) + + +def get_all_classes(services_path): + with open(services_path, "r", encoding="UTF-8") as file: + conditions_tree = ast.parse(file.read()) + + return [ + {"name": node.name, "body": ast.unparse(node)} + for node in conditions_tree.body + if isinstance(node, ast.ClassDef) + ] diff --git a/backend/chatsky_ui/services/json_converter_new2/node_converter.py b/backend/chatsky_ui/services/json_converter/node_converter.py similarity index 69% rename from backend/chatsky_ui/services/json_converter_new2/node_converter.py rename to backend/chatsky_ui/services/json_converter/node_converter.py index 27e4e738..9656479b 100644 --- a/backend/chatsky_ui/services/json_converter_new2/node_converter.py +++ b/backend/chatsky_ui/services/json_converter/node_converter.py @@ -1,11 +1,9 @@ -from typing import List +from chatsky import PRE_RESPONSE, PRE_TRANSITION, RESPONSE, TRANSITIONS -from .base_converter import BaseConverter from ...schemas.front_graph_components.node import InfoNode, LinkNode -from .logic_component_converter.response_converter import TextResponseConverter, CustomResponseConverter +from .base_converter import BaseConverter from .logic_component_converter.condition_converter import CustomConditionConverter, SlotConditionConverter - -from chatsky import RESPONSE, TRANSITIONS, PRE_TRANSITION +from .logic_component_converter.response_converter import CustomResponseConverter, TextResponseConverter class NodeConverter(BaseConverter): @@ -23,6 +21,13 @@ def __init__(self, config: dict): class InfoNodeConverter(NodeConverter): + MAP_TR2CHATSKY = { + "start": "dst.Start", + "fallback": "dst.Fallback", + "previous": "dst.Previous", + "repeat": "dst.Current", + } + def __init__(self, node: dict): self.node = InfoNode( id=node["id"], @@ -30,27 +35,33 @@ def __init__(self, node: dict): response=node["data"]["response"], conditions=node["data"]["conditions"], ) - + def __call__(self, *args, **kwargs): self.slots_conf = kwargs["slots_conf"] return super().__call__(*args, **kwargs) - + def _convert(self): - condition_converters = [self.CONDITION_CONVERTER[condition["type"]](condition) for condition in self.node.conditions] + condition_converters = [ + self.CONDITION_CONVERTER[condition["type"]](condition) for condition in self.node.conditions + ] return { RESPONSE: self.RESPONSE_CONVERTER[self.node.response["type"]](self.node.response)(), TRANSITIONS: [ { - "dst": condition["dst"], + "dst": condition["dst"] + if "dst" in condition and condition["data"]["transition_type"] == "manual" + else self.MAP_TR2CHATSKY[condition["data"]["transition_type"]], "priority": condition["data"]["priority"], - "cnd": converter(slots_conf=self.slots_conf) - } for condition, converter in zip(self.node.conditions, condition_converters) + "cnd": converter(slots_conf=self.slots_conf), + } + for condition, converter in zip(self.node.conditions, condition_converters) ], PRE_TRANSITION: { key: value for converter in condition_converters for key, value in converter.get_pre_transitions().items() - } + }, + PRE_RESPONSE: {"fill": {"chatsky.processing.FillTemplate": None}}, } @@ -71,24 +82,3 @@ def _convert(self): self.node.target_flow_name, self.mapped_flows[self.node.target_flow_name][self.node.target_node_id]["data"]["name"], ] - - -class ConfNodeConverter(NodeConverter): - def __init__(self, config: dict): - super().__init__(config) - - - def _convert(self): - return { - # node.name: node._convert() for node in self.nodes - } - - -class SlotsNodeConverter(ConfNodeConverter): - def __init__(self, config: List[dict]): - self.slots = config - - def _convert(self): - return { - # node.name: node._convert() for node in self.nodes - } diff --git a/backend/chatsky_ui/services/json_converter_new2/pipeline_converter.py b/backend/chatsky_ui/services/json_converter/pipeline_converter.py similarity index 79% rename from backend/chatsky_ui/services/json_converter_new2/pipeline_converter.py rename to backend/chatsky_ui/services/json_converter/pipeline_converter.py index 210f121f..969640fe 100644 --- a/backend/chatsky_ui/services/json_converter_new2/pipeline_converter.py +++ b/backend/chatsky_ui/services/json_converter/pipeline_converter.py @@ -1,25 +1,21 @@ from pathlib import Path + import yaml + try: - from yaml import CLoader as Loader, CDumper as Dumper + from yaml import CDumper as Dumper + from yaml import CLoader as Loader except ImportError: from yaml import Loader, Dumper from ...schemas.front_graph_components.pipeline import Pipeline -from ...schemas.front_graph_components.interface import Interface -from ...schemas.front_graph_components.flow import Flow - from .base_converter import BaseConverter -from .flow_converter import FlowConverter -from .script_converter import ScriptConverter from .interface_converter import InterfaceConverter +from .script_converter import ScriptConverter from .slots_converter import SlotsConverter class PipelineConverter(BaseConverter): - def __init__(self, pipeline_id: int): - self.pipeline_id = pipeline_id - def __call__(self, input_file: Path, output_dir: Path): self.from_yaml(file_path=input_file) @@ -33,7 +29,7 @@ def from_yaml(self, file_path: Path): self.graph = yaml.load(file, Loader=Loader) def to_yaml(self, dir_path: Path): - with open(f"{dir_path}/build_{self.pipeline_id}.yaml", "w", encoding="UTF-8") as file: + with open(f"{dir_path}/build.yaml", "w", encoding="UTF-8") as file: yaml.dump(self.converted_pipeline, file, Dumper=Dumper, default_flow_style=False) def _convert(self): diff --git a/backend/chatsky_ui/services/json_converter_new2/script_converter.py b/backend/chatsky_ui/services/json_converter/script_converter.py similarity index 76% rename from backend/chatsky_ui/services/json_converter_new2/script_converter.py rename to backend/chatsky_ui/services/json_converter/script_converter.py index dab6df66..f1f2197d 100644 --- a/backend/chatsky_ui/services/json_converter_new2/script_converter.py +++ b/backend/chatsky_ui/services/json_converter/script_converter.py @@ -1,14 +1,14 @@ from typing import List +from ...schemas.front_graph_components.script import Script from .base_converter import BaseConverter from .flow_converter import FlowConverter -from ...schemas.front_graph_components.script import Script class ScriptConverter(BaseConverter): def __init__(self, flows: List[dict]): self.script = Script(flows=flows) - self.mapped_flows = self._map_flows() #TODO: think about storing this in a temp file + self.mapped_flows = self._map_flows() # TODO: think about storing this in a temp file def __call__(self, *args, **kwargs): self.slots_conf = kwargs["slots_conf"] @@ -18,10 +18,7 @@ def _convert(self): return { key: value for flow in self.script.flows - for key, value in FlowConverter(flow)( - mapped_flows=self.mapped_flows, - slots_conf=self.slots_conf - ).items() + for key, value in FlowConverter(flow)(mapped_flows=self.mapped_flows, slots_conf=self.slots_conf).items() } def _map_flows(self): @@ -32,13 +29,13 @@ def _map_flows(self): mapped_flows[flow["name"]][node["id"]] = node return mapped_flows - def extract_start_fallback_labels(self): #TODO: refactor this huge method + def extract_start_fallback_labels(self): # TODO: refactor this huge method start_label, fallback_label = None, None - + for flow in self.script.flows: for node in flow["data"]["nodes"]: - flags = node["data"]["flags"] - + flags = node["data"].get("flags", []) + if "start" in flags: if start_label: raise ValueError("Multiple start nodes found") @@ -47,8 +44,8 @@ def extract_start_fallback_labels(self): #TODO: refactor this huge method if fallback_label: raise ValueError("Multiple fallback nodes found") fallback_label = [flow["name"], node["data"]["name"]] - + if start_label and fallback_label: return start_label, fallback_label - - return None, None + + return start_label, fallback_label # return None, None diff --git a/backend/chatsky_ui/services/json_converter_new2/slots_converter.py b/backend/chatsky_ui/services/json_converter/slots_converter.py similarity index 84% rename from backend/chatsky_ui/services/json_converter_new2/slots_converter.py rename to backend/chatsky_ui/services/json_converter/slots_converter.py index 1966b8d0..3bd9970b 100644 --- a/backend/chatsky_ui/services/json_converter_new2/slots_converter.py +++ b/backend/chatsky_ui/services/json_converter/slots_converter.py @@ -1,18 +1,17 @@ from typing import List -from .base_converter import BaseConverter -from ...schemas.front_graph_components.slot import GroupSlot, RegexpSlot from ...schemas.front_graph_components.node import SlotsNode +from ...schemas.front_graph_components.slot import GroupSlot, RegexpSlot +from .base_converter import BaseConverter + class SlotsConverter(BaseConverter): def __init__(self, flows: List[dict]): def _get_slots_node(flows): - return next(iter([ - node - for flow in flows - for node in flow["data"]["nodes"] - if node["type"] == "slots_node" - ])) + return next( + iter([node for flow in flows for node in flow["data"]["nodes"] if node["type"] == "slots_node"]), + {"id": "999999", "data": {"groups": []}}, + ) slots_node = _get_slots_node(flows) self.slots_node = SlotsNode( @@ -28,11 +27,8 @@ def map_slots(self): return mapped_slots def _convert(self): - return { - key: value - for group in self.slots_node.groups - for key, value in GroupSlotConverter(group)().items() - } + return {key: value for group in self.slots_node.groups for key, value in GroupSlotConverter(group)().items()} + class RegexpSlotConverter(SlotsConverter): def __init__(self, slot: dict): diff --git a/backend/chatsky_ui/services/json_converter_new2/consts.py b/backend/chatsky_ui/services/json_converter_new2/consts.py deleted file mode 100644 index d0219028..00000000 --- a/backend/chatsky_ui/services/json_converter_new2/consts.py +++ /dev/null @@ -1,3 +0,0 @@ -RESPONSES_FILE="responses" -CONDITIONS_FILE="conditions" -CUSTOM_FILE="custom" diff --git a/backend/chatsky_ui/services/json_converter_new2/logic_component_converter/response_converter.py b/backend/chatsky_ui/services/json_converter_new2/logic_component_converter/response_converter.py deleted file mode 100644 index 23e92639..00000000 --- a/backend/chatsky_ui/services/json_converter_new2/logic_component_converter/response_converter.py +++ /dev/null @@ -1,40 +0,0 @@ -import ast - -from ..base_converter import BaseConverter -from ....schemas.front_graph_components.info_holders.response import TextResponse, CustomResponse -from ..consts import CUSTOM_FILE, RESPONSES_FILE -from ....core.config import settings -from .service_replacer import store_custom_service - - -class ResponseConverter(BaseConverter): - pass - - -class TextResponseConverter(ResponseConverter): - def __init__(self, response: dict): - self.response = TextResponse( - name=response["name"], - text=next(iter(response["data"]))["text"], - ) - - def _convert(self): - return { - "chatsky.Message": { - "text": self.response.text - } - } - - -class CustomResponseConverter(ResponseConverter): - def __init__(self, response: dict): - self.response = CustomResponse( - name=response["name"], - code=next(iter(response["data"]))["python"]["action"], - ) - - def _convert(self): - store_custom_service(settings.responses_path, [self.response.code]) - return { - f"{CUSTOM_FILE}.{RESPONSES_FILE}.{self.response.name}": None - } diff --git a/backend/chatsky_ui/services/process.py b/backend/chatsky_ui/services/process.py index 1a042203..f5b18dda 100644 --- a/backend/chatsky_ui/services/process.py +++ b/backend/chatsky_ui/services/process.py @@ -7,17 +7,20 @@ import asyncio import logging import os +import signal from abc import ABC, abstractmethod from datetime import datetime from pathlib import Path from typing import Any, Dict, List, Optional from dotenv import load_dotenv +from httpx import AsyncClient from chatsky_ui.core.config import settings from chatsky_ui.core.logger_config import get_logger, setup_logging from chatsky_ui.db.base import read_conf, write_conf from chatsky_ui.schemas.process_status import Status +from chatsky_ui.utils.git_cmd import get_repo, save_built_script_to_git load_dotenv() @@ -25,14 +28,6 @@ PING_PONG_TIMEOUT = float(os.getenv("PING_PONG_TIMEOUT", 0.5)) -def _map_to_str(params: Dict[str, Any]): - for k, v in params.items(): - if isinstance(v, datetime): - params[k] = v.strftime("%Y-%m-%dT%H:%M:%S") - elif isinstance(v, Path): - params[k] = str(v) - - class Process(ABC): """Base for build and run processes.""" @@ -43,8 +38,9 @@ def __init__(self, id_: int, preset_end_status: str = ""): self.timestamp: datetime = datetime.now() self.log_path: Path self.lock: asyncio.Lock = asyncio.Lock() - self.process: asyncio.subprocess.Process # pylint: disable=no-member #TODO: is naming ok? + self.process: Optional[asyncio.subprocess.Process] = None self.logger: logging.Logger + self.to_be_terminated = False async def start(self, cmd_to_run: str) -> None: """Starts an asyncronous process with the given command.""" @@ -53,6 +49,7 @@ async def start(self, cmd_to_run: str) -> None: stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, stdin=asyncio.subprocess.PIPE, + preexec_fn=os.setsid, ) async def get_full_info(self, attributes: list) -> Dict[str, Any]: @@ -65,12 +62,21 @@ async def get_full_info(self, attributes: list) -> Dict[str, Any]: Returns: dict: A dictionary containing the values of the attributes mentioned in the list. """ + + def _map_to_str(params: Dict[str, Any]): + for k, v in params.copy().items(): + if isinstance(v, datetime): + params[k] = v.strftime("%Y-%m-%dT%H:%M:%S") + elif isinstance(v, Path): + params[k] = str(v) + return params + await self.check_status() info = {key: getattr(self, key) for key in self.__dict__ if key in attributes} if "status" in attributes: info["status"] = self.status.value - return info + return _map_to_str(info) @abstractmethod async def update_db_info(self): @@ -78,10 +84,16 @@ async def update_db_info(self): async def periodically_check_status(self) -> None: """Periodically checks the process status and updates the database.""" - while True: + while not self.to_be_terminated: await self.update_db_info() # check status and update db self.logger.info("Status of process '%s': %s", self.id, self.status) - if self.status in [Status.STOPPED, Status.COMPLETED, Status.FAILED]: + if self.status in [ + Status.NULL, + Status.STOPPED, + Status.COMPLETED, + Status.FAILED, + Status.FAILED_WITH_UNEXPECTED_CODE, + ]: break await asyncio.sleep(2) # TODO: ?sleep time shouldn't be constant @@ -100,6 +112,7 @@ async def check_status(self) -> Status: """ if self.process is None: self.status = Status.NULL + return self.status # if process is already alive, don't interrupt potential open channels by checking status periodically. elif self.process.returncode is None: if self.status == Status.ALIVE: @@ -123,7 +136,7 @@ async def check_status(self) -> Status: ) self.status = Status.FAILED_WITH_UNEXPECTED_CODE - if self.status not in [Status.NULL, Status.RUNNING, Status.ALIVE, Status.STOPPED]: + if self.status not in [Status.NULL, Status.RUNNING, Status.ALIVE]: stdout, stderr = await self.process.communicate() if stdout: self.logger.info(f"[stdout]\n{stdout.decode()}") @@ -143,55 +156,29 @@ async def stop(self) -> None: self.logger.error("Cannot stop a process '%s' that has not started yet.", self.id) raise RuntimeError try: - self.logger.debug("Terminating process '%s'", self.id) - self.process.terminate() + self.logger.debug("Terminating process '%s' with group process pid of '%s'", self.id, self.process.pid) + os.killpg(os.getpgid(self.process.pid), signal.SIGTERM) try: await asyncio.wait_for(self.process.wait(), timeout=GRACEFUL_TERMINATION_TIMEOUT) self.logger.debug("Process '%s' was gracefully terminated.", self.id) except asyncio.TimeoutError: - self.process.kill() - await self.process.wait() + os.killpg(os.getpgid(self.process.pid), signal.SIGKILL) self.logger.debug("Process '%s' was forcefully killed.", self.id) self.logger.debug("Process returencode '%s' ", self.process.returncode) - except ProcessLookupError as exc: - self.logger.error("Process '%s' not found. It may have already exited.", self.id) + self.logger.error("Process group '%s' not found. It may have already exited.", self.id) raise ProcessLookupError from exc - async def read_stdout(self) -> bytes: - """Reads the stdout of the process for communication.""" - async with self.lock: - if self.process is None: - self.logger.error("Cannot read stdout from a process '%s' that has not started yet.", self.id) - raise RuntimeError - if self.process.stdout is None: - raise RuntimeError(f"The process '{self.id}' stdout is None. It might be still running.") - return await self.process.stdout.readline() - - async def write_stdin(self, message: bytes) -> None: - """Writes a message to the stdin of the process for communication.""" - if self.process is None: - self.logger.error("Cannot write into stdin of a process '%s' that has not started yet.", self.id) - raise RuntimeError - if self.process.stdin is None: - raise RuntimeError(f"The process '{self.id}' stdin is None. It might be still running.") - self.process.stdin.write(message) - await self.process.stdin.drain() + def add_new_conf(self, conf: list, params: dict) -> list: # TODO: rename conf everywhere to metadata/meta + for run in conf: + if run.id == params["id"]: # type: ignore + for key, value in params.items(): + setattr(run, key, value) + break + else: + conf.append(params) - async def is_alive(self) -> bool: - """Checks if the process is alive by writing to stdin andreading its stdout.""" - message = b"Hi\n" - try: - # Attempt to write and read from the process with a timeout. - await self.write_stdin(message) - output = await asyncio.wait_for(self.read_stdout(), timeout=PING_PONG_TIMEOUT) - if not output: - return False - self.logger.debug("Process is alive and output afer communication is: %s", output.decode()) - return True - except asyncio.exceptions.TimeoutError: - self.logger.debug("Process is still running.") - return False + return conf class RunProcess(Process): @@ -213,17 +200,9 @@ async def update_db_info(self) -> None: # save current run info into runs_path self.logger.debug("Updating db run info") runs_conf = await read_conf(settings.runs_path) - run_params = await self.get_full_info() - _map_to_str(run_params) - for run in runs_conf: - if run.id == run_params["id"]: # type: ignore - for key, value in run_params.items(): - setattr(run, key, value) - break - else: - runs_conf.append(run_params) + runs_conf = self.add_new_conf(runs_conf, run_params) # type: ignore await write_conf(runs_conf, settings.runs_path) @@ -237,6 +216,50 @@ async def update_db_info(self) -> None: await write_conf(builds_conf, settings.builds_path) + async def is_alive(self) -> bool: + """Checks if the process is alive by writing to stdin andreading its stdout.""" + + async def check_telegram_readiness(stream, name): + async for line in stream: + decoded_line = line.decode().strip() + self.logger.info(f"[{name}] {decoded_line}") + + if "telegram.ext.Application:Application started" in decoded_line: + self.logger.info("The application is ready for use!") + return True + return False + + async with AsyncClient() as client: + try: + response = await client.get( + f"http://localhost:{settings.chatsky_port}/health", + ) + return response.json()["status"] == "ok" + except Exception as e: + self.logger.info( + f"Process '{self.id}' isn't alive on port '{settings.chatsky_port}' yet. " + f"Ignore this if you're not connecting via HTTPInterface. Exception caught: {e}" + ) + + done, pending = await asyncio.wait( + [ + asyncio.create_task(check_telegram_readiness(self.process.stdout, "STDOUT")), + asyncio.create_task(check_telegram_readiness(self.process.stderr, "STDERR")), + ], + return_when=asyncio.FIRST_COMPLETED, + timeout=PING_PONG_TIMEOUT, + ) + + for task in pending: + task.cancel() + + for task in done: + result = task.result() + if result: + return result + + return False + class BuildProcess(Process): """Process for converting a frontned graph to a Chatsky script.""" @@ -254,18 +277,27 @@ async def get_full_info(self, attributes: Optional[list] = None) -> Dict[str, An return await super().get_full_info(attributes) async def update_db_info(self) -> None: - # save current build info into builds_path + """Saves current build info into builds_path""" builds_conf = await read_conf(settings.builds_path) - build_params = await self.get_full_info() - _map_to_str(build_params) - for build in builds_conf: - if build.id == build_params["id"]: # type: ignore - for key, value in build_params.items(): - setattr(build, key, value) - break - else: - builds_conf.append(build_params) + builds_conf = self.add_new_conf(builds_conf, build_params) # type: ignore await write_conf(builds_conf, settings.builds_path) + + def save_built_script_to_git(self, id_: int) -> None: + bot_repo = get_repo(settings.custom_dir.parent) + save_built_script_to_git(id_, bot_repo) + + async def periodically_check_status(self) -> None: + """Periodically checks the process status and updates the database.""" + while not self.to_be_terminated: + await self.update_db_info() # check status and update db + self.logger.info("Status of process '%s': %s", self.id, self.status) + if self.status in [Status.NULL, Status.STOPPED, Status.COMPLETED, Status.FAILED]: + self.save_built_script_to_git(self.id) + break + await asyncio.sleep(2) # TODO: ?sleep time shouldn't be constant + + async def is_alive(self) -> bool: + return False diff --git a/backend/chatsky_ui/services/process_manager.py b/backend/chatsky_ui/services/process_manager.py index 3a6aeceb..6f8744e2 100644 --- a/backend/chatsky_ui/services/process_manager.py +++ b/backend/chatsky_ui/services/process_manager.py @@ -6,9 +6,11 @@ starting, stopping, updating, and checking status of processes. Processes themselves are stored in the `processes` dictionary of process managers. """ +import os from pathlib import Path from typing import Any, Dict, List, Optional, Union +from dotenv import load_dotenv from omegaconf import OmegaConf from chatsky_ui.core.config import settings @@ -17,6 +19,7 @@ from chatsky_ui.schemas.preset import Preset from chatsky_ui.schemas.process_status import Status from chatsky_ui.services.process import BuildProcess, RunProcess +from chatsky_ui.utils.git_cmd import get_repo, save_frontend_graph_to_git class ProcessManager: @@ -56,10 +59,10 @@ async def stop(self, id_: int) -> None: raise async def stop_all(self) -> None: - self.logger.info("Stopping all process %s", self.processes) for id_, process in self.processes.items(): - if process.process.returncode is None: + if await process.check_status() in [Status.ALIVE, Status.RUNNING]: await self.stop(id_) + await process.update_db_info() async def check_status(self, id_: int, *args, **kwargs) -> None: """Checks the status of the process with the given id by calling the `periodically_check_status` @@ -134,6 +137,8 @@ async def start(self, build_id: int, preset: Preset) -> int: self.last_id += 1 id_ = self.last_id process = RunProcess(id_, build_id, preset.end_status) + + load_dotenv(os.path.join(settings.work_directory, ".env"), override=True) await process.start(cmd_to_run) process.logger.debug("Started process. status: '%s'", process.process.returncode) self.processes[id_] = process @@ -175,26 +180,45 @@ async def start(self, preset: Preset) -> int: self.last_id = max([build["id"] for build in await self.get_full_info(0, 10000)]) self.last_id += 1 id_ = self.last_id + + if self.is_repeated_id(id_): + raise ValueError(f"Build id '{id_}' already exists in the database") + process = BuildProcess(id_, preset.end_status) - cmd_to_run = ( - f"chatsky.ui build_bot --build-id {id_} " - f"--preset {preset.end_status} " - f"--project-dir {settings.work_directory}" - ) - await process.start(cmd_to_run) + if self.is_changed_graph(id_): + cmd_to_run = ( + f"chatsky.ui build_bot " f"--preset {preset.end_status} " f"--project-dir {settings.work_directory}" + ) + await process.start(cmd_to_run) self.processes[id_] = process return self.last_id - async def check_status(self, id_, index, *args, **kwargs): + async def check_status(self, id_, *args, **kwargs): """Checks the build "id_" process status by calling the `periodically_check_status` method of the process. This updates the process status in the database every 2 seconds. - The index is refreshed after the build is done/failed. """ await self.processes[id_].periodically_check_status() - await index.load() + + def is_repeated_id(self, id_: int) -> bool: + bot_repo = get_repo(settings.custom_dir.parent) + + for tag in bot_repo.tags: + if tag.name == str(id_): + return True + return False + + def is_changed_graph(self, id_: int) -> bool: + chatsky_ui_repo = get_repo(settings.frontend_flows_path.parent) + is_changed = save_frontend_graph_to_git(id_, chatsky_ui_repo) + if is_changed: + self.logger.info("Graph is changed. Gonna build") + return True + else: + self.logger.info("Graph isn't changed. Ain't gonna build") + return False async def get_build_info(self, id_: int, run_manager: RunManager) -> Optional[Dict[str, Any]]: """Returns metadata of a specific build process identified by its unique ID. diff --git a/backend/chatsky_ui/services/websocket_manager.py b/backend/chatsky_ui/services/websocket_manager.py deleted file mode 100644 index 70b57b8c..00000000 --- a/backend/chatsky_ui/services/websocket_manager.py +++ /dev/null @@ -1,80 +0,0 @@ -""" -Websocket class for controling websocket operations. -""" -import asyncio -from asyncio.tasks import Task -from typing import Dict, List, Set - -from fastapi import WebSocket, WebSocketDisconnect - -from chatsky_ui.core.logger_config import get_logger -from chatsky_ui.services.process_manager import ProcessManager - - -class WebSocketManager: - """Controls websocket operations connect, disconnect, check status, and communicate.""" - - def __init__(self): - self.pending_tasks: Dict[WebSocket, Set[Task]] = dict() - self.active_connections: List[WebSocket] = [] - self._logger = None - - @property - def logger(self): - if self._logger is None: - raise ValueError("Logger has not been configured. Call set_logger() first.") - return self._logger - - def set_logger(self): - self._logger = get_logger(__name__) - - async def connect(self, websocket: WebSocket): - """Accepts the websocket connection and marks it as active connection.""" - await websocket.accept() - self.active_connections.append(websocket) - - def disconnect(self, websocket: WebSocket): - """Cancels pending tasks of the open websocket process and removes it from active connections.""" - # TODO: await websocket.close() - if websocket in self.pending_tasks: - self.logger.info("Cancelling pending tasks") - for task in self.pending_tasks[websocket]: - task.cancel() - del self.pending_tasks[websocket] - self.active_connections.remove(websocket) - - def check_status(self, websocket: WebSocket): - if websocket in self.active_connections: - return websocket # return Status! - - async def send_process_output_to_websocket( - self, run_id: int, process_manager: ProcessManager, websocket: WebSocket - ): - """Reads and forwards process output to the websocket client.""" - try: - while True: - response = await process_manager.processes[run_id].read_stdout() - if not response: - break - await websocket.send_text(response.decode().strip().split("text=")[-1].strip("'")) - except WebSocketDisconnect: - self.logger.info("Websocket connection is closed by client") - except RuntimeError: - raise - - async def forward_websocket_messages_to_process( - self, run_id: int, process_manager: ProcessManager, websocket: WebSocket - ): - """Listens for messages from the websocket and sends them to the process.""" - try: - while True: - user_message = await websocket.receive_text() - if not user_message: - break - await process_manager.processes[run_id].write_stdin(user_message.encode() + b"\n") - except asyncio.CancelledError: - self.logger.info("Websocket connection is closed") - except WebSocketDisconnect: - self.logger.info("Websocket connection is closed by client") - except RuntimeError: - raise diff --git a/backend/chatsky_ui/tests/api/test_bot.py b/backend/chatsky_ui/tests/api/test_bot.py index f50529c4..20f70b5e 100644 --- a/backend/chatsky_ui/tests/api/test_bot.py +++ b/backend/chatsky_ui/tests/api/test_bot.py @@ -6,7 +6,6 @@ _stop_process, check_build_processes, check_run_processes, - connect, get_build_logs, get_run_logs, start_build, @@ -16,8 +15,6 @@ from chatsky_ui.services.process_manager import RunManager PROCESS_ID = 0 -RUN_ID = 42 -BUILD_ID = 43 @pytest.mark.asyncio @@ -63,27 +60,27 @@ async def test_check_process_status(mocker): @pytest.mark.asyncio -async def test_start_build(mocker): +async def test_start_build(mocker, dummy_build_id): build_manager = mocker.MagicMock() preset = mocker.MagicMock() - start = mocker.AsyncMock(return_value=BUILD_ID) + start = mocker.AsyncMock(return_value=dummy_build_id) mocker.patch.multiple(build_manager, start=start, check_status=mocker.AsyncMock()) mocker.patch.multiple(preset, wait_time=0, end_status="loop") response = await start_build(preset, background_tasks=BackgroundTasks(), build_manager=build_manager) start.assert_awaited_once_with(preset) - assert response == {"status": "ok", "build_id": BUILD_ID} + assert response == {"status": "ok", "build_id": dummy_build_id} @pytest.mark.asyncio -async def test_check_build_processes_some_info(mocker, pagination): +async def test_check_build_processes_some_info(mocker, pagination, dummy_build_id): build_manager = mocker.AsyncMock() run_manager = mocker.AsyncMock() - await check_build_processes(BUILD_ID, build_manager, run_manager, pagination) + await check_build_processes(dummy_build_id, build_manager, run_manager, pagination) - build_manager.get_build_info.assert_awaited_once_with(BUILD_ID, run_manager) + build_manager.get_build_info.assert_awaited_once_with(dummy_build_id, run_manager) @pytest.mark.asyncio @@ -100,37 +97,37 @@ async def test_check_build_processes_all_info(mocker, pagination): @pytest.mark.asyncio -async def test_get_build_logs(mocker, pagination): +async def test_get_build_logs(mocker, pagination, dummy_build_id): build_manager = mocker.AsyncMock() - await get_build_logs(BUILD_ID, build_manager, pagination) + await get_build_logs(dummy_build_id, build_manager, pagination) - build_manager.fetch_build_logs.assert_awaited_once_with(BUILD_ID, pagination.offset(), pagination.limit) + build_manager.fetch_build_logs.assert_awaited_once_with(dummy_build_id, pagination.offset(), pagination.limit) @pytest.mark.asyncio -async def test_start_run(mocker): +async def test_start_run(mocker, dummy_build_id, dummy_run_id): run_manager = mocker.MagicMock() preset = mocker.MagicMock() - start = mocker.AsyncMock(return_value=RUN_ID) + start = mocker.AsyncMock(return_value=dummy_run_id) mocker.patch.multiple(run_manager, start=start, check_status=mocker.AsyncMock()) mocker.patch.multiple(preset, wait_time=0, end_status="loop") response = await start_run( - build_id=BUILD_ID, preset=preset, background_tasks=BackgroundTasks(), run_manager=run_manager + build_id=dummy_build_id, preset=preset, background_tasks=BackgroundTasks(), run_manager=run_manager ) - start.assert_awaited_once_with(BUILD_ID, preset) - assert response == {"status": "ok", "run_id": RUN_ID} + start.assert_awaited_once_with(dummy_build_id, preset) + assert response == {"status": "ok", "run_id": dummy_run_id} @pytest.mark.asyncio -async def test_check_run_processes_some_info(mocker, pagination): +async def test_check_run_processes_some_info(mocker, pagination, dummy_run_id): run_manager = mocker.AsyncMock() - await check_run_processes(RUN_ID, run_manager, pagination) + await check_run_processes(dummy_run_id, run_manager, pagination) - run_manager.get_run_info.assert_awaited_once_with(RUN_ID) + run_manager.get_run_info.assert_awaited_once_with(dummy_run_id) @pytest.mark.asyncio @@ -144,27 +141,9 @@ async def test_check_run_processes_all_info(mocker, pagination): @pytest.mark.asyncio -async def test_get_run_logs(mocker, pagination): +async def test_get_run_logs(mocker, pagination, dummy_run_id): run_manager = mocker.AsyncMock() - await get_run_logs(RUN_ID, run_manager, pagination) + await get_run_logs(dummy_run_id, run_manager, pagination) - run_manager.fetch_run_logs.assert_awaited_once_with(RUN_ID, pagination.offset(), pagination.limit) - - -@pytest.mark.asyncio -async def test_connect(mocker): - websocket = mocker.AsyncMock() - websocket_manager = mocker.AsyncMock() - websocket_manager.disconnect = mocker.MagicMock() - run_manager = mocker.AsyncMock() - run_process = mocker.AsyncMock() - run_manager.processes = {RUN_ID: run_process} - mocker.patch.object(websocket, "query_params", {"run_id": str(RUN_ID)}) - - await connect(websocket, websocket_manager, run_manager) - - websocket_manager.connect.assert_awaited_once_with(websocket) - websocket_manager.send_process_output_to_websocket.assert_awaited_once_with(RUN_ID, run_manager, websocket) - websocket_manager.forward_websocket_messages_to_process.assert_awaited_once_with(RUN_ID, run_manager, websocket) - websocket_manager.disconnect.assert_called_once_with(websocket) + run_manager.fetch_run_logs.assert_awaited_once_with(dummy_run_id, pagination.offset(), pagination.limit) diff --git a/backend/chatsky_ui/tests/conftest.py b/backend/chatsky_ui/tests/conftest.py index 662ec4a2..18046da9 100644 --- a/backend/chatsky_ui/tests/conftest.py +++ b/backend/chatsky_ui/tests/conftest.py @@ -15,11 +15,18 @@ from chatsky_ui.main import app from chatsky_ui.schemas.pagination import Pagination from chatsky_ui.schemas.preset import Preset -from chatsky_ui.services.process import RunProcess +from chatsky_ui.services.process import BuildProcess, RunProcess from chatsky_ui.services.process_manager import BuildManager, RunManager -from chatsky_ui.services.websocket_manager import WebSocketManager -DUMMY_BUILD_ID = -1 + +@pytest.fixture(scope="session") +def dummy_build_id() -> int: + return 999999 + + +@pytest.fixture(scope="session") +def dummy_run_id() -> int: + return 999999 async def start_process(async_client: AsyncClient, endpoint, preset_end_status) -> httpx.Response: @@ -29,18 +36,22 @@ async def start_process(async_client: AsyncClient, endpoint, preset_end_status) ) -@asynccontextmanager -async def override_dependency(mocker_obj, get_manager_func): - process_manager = get_manager_func() - process_manager.check_status = mocker_obj.AsyncMock() - app.dependency_overrides[get_manager_func] = lambda: process_manager - try: - yield process_manager - finally: - for _, process in process_manager.processes.items(): - if process.process.returncode is None: - await process.stop() - app.dependency_overrides = {} +@pytest.fixture +def override_dependency(mocker): + @asynccontextmanager + async def _override_dependency(get_manager_func): + process_manager = get_manager_func() + process_manager.check_status = mocker.AsyncMock() + app.dependency_overrides[get_manager_func] = lambda: process_manager + try: + yield process_manager + finally: + for _, process in process_manager.processes.items(): + if process.process.returncode is None: + await process.stop() + app.dependency_overrides = {} + + return _override_dependency @pytest.fixture @@ -63,15 +74,25 @@ def pagination() -> Pagination: @pytest.fixture() -def run_process(): - async def _run_process(cmd_to_run): - process = RunProcess(id_=0, build_id=DUMMY_BUILD_ID) +def run_process(dummy_build_id, dummy_run_id): + async def _run_process(cmd_to_run) -> RunProcess: + process = RunProcess(id_=dummy_run_id, build_id=dummy_build_id) await process.start(cmd_to_run) return process return _run_process +@pytest.fixture() +def build_process(dummy_build_id): + async def _build_process(cmd_to_run) -> BuildProcess: + process = BuildProcess(id_=dummy_build_id) + await process.start(cmd_to_run) + return process + + return _build_process + + @pytest.fixture() def run_manager(): manager = RunManager() @@ -82,10 +103,3 @@ def run_manager(): @pytest.fixture() def build_manager(): return BuildManager() - - -@pytest.fixture -def websocket_manager(): - manager = WebSocketManager() - manager.set_logger() - return manager diff --git a/backend/chatsky_ui/tests/integration/test_api_integration.py b/backend/chatsky_ui/tests/integration/test_api_integration.py index 6ab9716a..00c97267 100644 --- a/backend/chatsky_ui/tests/integration/test_api_integration.py +++ b/backend/chatsky_ui/tests/integration/test_api_integration.py @@ -99,12 +99,12 @@ async def _test_stop_inexistent_process(mocker, get_manager_func, start_endpoint # Test flows endpoints and interaction with db (read and write conf) def test_flows(client): # noqa: F811 - get_response = client.get("/api/v1/flows") + get_response = client.get("/api/v1/flows/43") assert get_response.status_code == 200 data = get_response.json()["data"] assert "flows" in data - response = client.post("/api/v1/flows", json=data) + response = client.post("/api/v1/flows/test_save1", json=data) assert response.status_code == 200 @@ -151,7 +151,7 @@ async def test_stop_build_bad_id(mocker): "end_status, process_status", [("failure", Status.FAILED), ("loop", Status.RUNNING), ("success", Status.ALIVE)] ) async def test_start_run(mocker, end_status, process_status): - build_id = 43 + build_id = 0 await _test_start_process( mocker, get_run_manager, @@ -164,7 +164,7 @@ async def test_start_run(mocker, end_status, process_status): @pytest.mark.asyncio async def test_stop_run(mocker): - build_id = 43 + build_id = 0 await _test_stop_process( mocker, get_run_manager, @@ -175,7 +175,7 @@ async def test_stop_run(mocker): @pytest.mark.asyncio async def test_stop_run_bad_id(mocker): - build_id = 43 + build_id = 0 await _test_stop_inexistent_process( mocker, get_run_manager, @@ -186,7 +186,7 @@ async def test_stop_run_bad_id(mocker): @pytest.mark.asyncio async def test_connect_to_ws(mocker): - build_id = 43 + build_id = 0 async with httpx.AsyncClient(transport=ASGIWebSocketTransport(app)) as client: async with override_dependency(mocker, get_run_manager) as process_manager: diff --git a/backend/chatsky_ui/tests/integration/test_bot.py b/backend/chatsky_ui/tests/integration/test_bot.py new file mode 100644 index 00000000..4908c8f5 --- /dev/null +++ b/backend/chatsky_ui/tests/integration/test_bot.py @@ -0,0 +1,97 @@ +import asyncio +import os + +import pytest +from dotenv import load_dotenv +from httpx import AsyncClient +from httpx._transports.asgi import ASGITransport + +from chatsky_ui.api.deps import get_build_manager, get_run_manager +from chatsky_ui.core.logger_config import get_logger +from chatsky_ui.main import app +from chatsky_ui.schemas.process_status import Status + +load_dotenv() + +BUILD_COMPLETION_TIMEOUT = float(os.getenv("BUILD_COMPLETION_TIMEOUT", 10)) +RUN_RUNNING_TIMEOUT = float(os.getenv("RUN_RUNNING_TIMEOUT", 5)) + +logger = get_logger(__name__) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "preset_status, expected_status", + [("failure", Status.FAILED), ("loop", Status.RUNNING), ("success", Status.COMPLETED)], +) +async def test_start_build(mocker, override_dependency, preset_status, expected_status): + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as async_client: + async with override_dependency(get_build_manager) as process_manager: + process_manager.save_built_script_to_git = mocker.MagicMock() + process_manager.is_changed_graph = mocker.MagicMock(return_value=True) + + response = await async_client.post( + "/api/v1/bot/build/start", + json={"wait_time": 0.1, "end_status": preset_status}, + ) + + assert response.json().get("status") == "ok", "Start process response status is not 'ok'" + + process_id = process_manager.last_id + process = process_manager.processes[process_id] + + try: + await asyncio.wait_for(process.process.wait(), timeout=BUILD_COMPLETION_TIMEOUT) + except asyncio.exceptions.TimeoutError as exc: + if preset_status == "loop": + logger.debug("Loop process timed out. Expected behavior.") + assert True + await process.stop() + return + else: + raise Exception( + f"Process with expected end status '{preset_status}' timed out with " + f"return code '{process.process.returncode}'." + ) from exc + + current_status = await process_manager.get_status(process_id) + assert ( + current_status == expected_status + ), f"Current process status '{current_status}' did not match the expected '{expected_status}'" + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "preset_status, expected_status", [("failure", Status.FAILED), ("loop", Status.RUNNING), ("success", Status.ALIVE)] +) +async def test_start_run(override_dependency, preset_status, expected_status, dummy_build_id): + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as async_client: + async with override_dependency(get_run_manager) as process_manager: + response = await async_client.post( + f"/api/v1/bot/run/start/{dummy_build_id}", + json={"wait_time": 0.1, "end_status": preset_status}, + ) + + assert response.json().get("status") == "ok", "Start process response status is not 'ok'" + + process_id = process_manager.last_id + process = process_manager.processes[process_id] + + try: + await asyncio.wait_for(process.process.wait(), timeout=RUN_RUNNING_TIMEOUT) + except asyncio.exceptions.TimeoutError as exc: + if preset_status == "loop": + logger.debug("Loop process timed out. Expected behavior.") + assert True + await process.stop() + return + else: + raise Exception( + f"Process with expected end status '{preset_status}' timed out with " + f"return code '{process.process.returncode}'." + ) from exc + + current_status = await process_manager.get_status(process_id) + assert ( + current_status == expected_status + ), f"Current process status '{current_status}' did not match the expected '{expected_status}'" diff --git a/backend/chatsky_ui/tests/services/test_process.py b/backend/chatsky_ui/tests/services/test_process.py index c0c958db..2fb1af42 100644 --- a/backend/chatsky_ui/tests/services/test_process.py +++ b/backend/chatsky_ui/tests/services/test_process.py @@ -2,10 +2,19 @@ import pytest +from chatsky_ui.core.config import settings +from chatsky_ui.db.base import read_conf from chatsky_ui.schemas.process_status import Status class TestRunProcess: + @pytest.mark.asyncio + async def test_get_full_info(self, run_process): + process = await run_process("sleep 10000") + await asyncio.sleep(2) + info = await process.get_full_info(["status", "timestamp"]) + assert info["status"] == Status.RUNNING.value + @pytest.mark.asyncio @pytest.mark.parametrize( "cmd_to_run, status", @@ -20,10 +29,6 @@ async def test_check_status(self, run_process, cmd_to_run, status): await asyncio.sleep(2) assert await process.check_status() == status - # def test_periodically_check_status(self, run_process): - # process = await run_process("sleep 10000") - # run_process.periodically_check_status() - @pytest.mark.asyncio async def test_stop(self, run_process): process = await run_process("sleep 10000") @@ -31,18 +36,19 @@ async def test_stop(self, run_process): assert process.process.returncode == -15 @pytest.mark.asyncio - async def test_read_stdout(self, run_process): + async def test_update_db_info(self, run_process, dummy_run_id): process = await run_process("echo Hello") - output = await process.read_stdout() - assert output.strip().decode() == "Hello" + await process.update_db_info() - @pytest.mark.asyncio - async def test_write_stdout(self, run_process): - process = await run_process("cat") - await process.write_stdin(b"Chatsky-UI team welcome you.\n") - output = await process.process.stdout.readline() - assert output.decode().strip() == "Chatsky-UI team welcome you." + runs_conf = await read_conf(settings.runs_path) + assert dummy_run_id in [conf["id"] for conf in runs_conf] # type: ignore -# class TestBuildProcess: -# pass +class TestBuildProcess: + @pytest.mark.asyncio + async def test_update_db_info(self, build_process, dummy_build_id): + process = await build_process("echo Hello") + await process.update_db_info() + + builds_conf = await read_conf(settings.builds_path) + assert dummy_build_id in [conf["id"] for conf in builds_conf] # type: ignore diff --git a/backend/chatsky_ui/tests/services/test_process_manager.py b/backend/chatsky_ui/tests/services/test_process_manager.py index 11799169..3215a307 100644 --- a/backend/chatsky_ui/tests/services/test_process_manager.py +++ b/backend/chatsky_ui/tests/services/test_process_manager.py @@ -4,7 +4,7 @@ from omegaconf import OmegaConf RUN_ID = 42 -BUILD_ID = 43 +BUILD_ID = 0 class TestRunManager: diff --git a/backend/chatsky_ui/tests/services/test_websocket_manager.py b/backend/chatsky_ui/tests/services/test_websocket_manager.py deleted file mode 100644 index ada2589a..00000000 --- a/backend/chatsky_ui/tests/services/test_websocket_manager.py +++ /dev/null @@ -1,57 +0,0 @@ -import pytest -from fastapi import WebSocket - - -class TestWebSocketManager: - @pytest.mark.asyncio - async def test_connect(self, mocker, websocket_manager): - mocked_websocket = mocker.MagicMock(spec=WebSocket) - - await websocket_manager.connect(mocked_websocket) - - mocked_websocket.accept.assert_awaited_once_with() - assert mocked_websocket in websocket_manager.active_connections - - @pytest.mark.asyncio - async def test_disconnect(self, mocker, websocket_manager): - mocked_websocket = mocker.MagicMock(spec=WebSocket) - websocket_manager.active_connections.append(mocked_websocket) - websocket_manager.pending_tasks[mocked_websocket] = set() - - websocket_manager.disconnect(mocked_websocket) - - assert mocked_websocket not in websocket_manager.pending_tasks - assert mocked_websocket not in websocket_manager.active_connections - - @pytest.mark.asyncio - async def test_send_process_output_to_websocket(self, mocker, websocket_manager): - run_id = 42 - awaited_response = "Hello from DF-Designer" - - websocket = mocker.AsyncMock() - run_manager = mocker.MagicMock() - run_process = mocker.MagicMock() - run_process.read_stdout = mocker.AsyncMock(side_effect=[awaited_response.encode(), None]) - run_manager.processes = {run_id: run_process} - - await websocket_manager.send_process_output_to_websocket(run_id, run_manager, websocket) - - assert run_process.read_stdout.call_count == 2 - websocket.send_text.assert_awaited_once_with(awaited_response) - - @pytest.mark.asyncio - async def test_forward_websocket_messages_to_process(self, mocker, websocket_manager): - run_id = 42 - awaited_message = "Hello from DF-Designer" - - websocket = mocker.AsyncMock() - websocket.receive_text = mocker.AsyncMock(side_effect=[awaited_message, None]) - run_manager = mocker.MagicMock() - run_process = mocker.MagicMock() - run_process.write_stdin = mocker.AsyncMock() - run_manager.processes = {run_id: run_process} - - await websocket_manager.forward_websocket_messages_to_process(run_id, run_manager, websocket) - - assert websocket.receive_text.await_count == 2 - run_process.write_stdin.assert_called_once_with(awaited_message.encode() + b"\n") diff --git a/backend/chatsky_ui/tests/unit/__init__.py b/backend/chatsky_ui/tests/unit/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/chatsky_ui/tests/unit/conftest.py b/backend/chatsky_ui/tests/unit/conftest.py new file mode 100644 index 00000000..28008cfd --- /dev/null +++ b/backend/chatsky_ui/tests/unit/conftest.py @@ -0,0 +1,146 @@ +import pytest +from chatsky import PRE_RESPONSE, PRE_TRANSITION, RESPONSE, TRANSITIONS + + +@pytest.fixture +def custom_condition(): + return { + "id": "condition1", + "name": "test_condition", + "type": "python", + "data": { + "priority": 1, + "transition_type": "manual", + "python": { + "action": """class test_condition(BaseCondition):\n async def call(self, ctx: + Context) -> bool:\n return True""" + }, + }, + "dst": "dst_test_node", + } + + +@pytest.fixture +def converted_custom_condition(): + return {"custom.conditions.test_condition": None} + + +@pytest.fixture +def custom_response(): + return { + "name": "test_response", + "type": "python", + "data": [ + { + "python": { + "action": """class test_response(BaseResponse):\n async def call(self, ctx: + Context) -> Message:\n return Message('Hello')""" + } + } + ], + } + + +@pytest.fixture +def converted_custom_response(): + return {"custom.responses.test_response": None} + + +@pytest.fixture +def slots_conf(): + return {"test_slot": "test_slot"} + + +# @pytest.fixture +# def converted_pre_transition(): +# return { +# "test_slot": { +# "chatsky.processing.slots.Extract": "test_slot" +# } +# } + + +@pytest.fixture +def regexp_slot(): + return { + "id": "test_slot_id", + "name": "test_slot", + "type": "RegexpSlot", + "value": "test_regexp_value", + "match_group_idx": 1, + } + + +@pytest.fixture +def converted_regexp_slot(): + return { + "test_slot": { + "chatsky.slots.RegexpSlot": { + "regexp": "test_regexp_value", + "match_group_idx": 1, + } + } + } + + +@pytest.fixture +def group_slot(regexp_slot): + return {"name": "group_slot", "slots": [regexp_slot]} + + +@pytest.fixture +def converted_group_slot(converted_regexp_slot): + return {"group_slot": converted_regexp_slot} + + +@pytest.fixture +def info_node(custom_response, custom_condition): + return { + "id": "1", + "type": "default_node", + "data": { + "name": "test_node", + "response": custom_response, + "conditions": [custom_condition], + "flags": ["start", "fallback"], + }, + } + + +@pytest.fixture +def flow(info_node, group_slot): + return { + "name": "test_flow", + "data": { + "nodes": [ + info_node, + {"type": "slots_node", "id": "test_slots_node_id", "data": {"groups": [group_slot]}}, + ], + "edges": [{"source": "1", "sourceHandle": "1", "target": "1"}], + }, + } + + +@pytest.fixture +def chatsky_node(converted_custom_response, converted_custom_condition): + return { + PRE_RESPONSE: {"fill": {"chatsky.processing.FillTemplate": None}}, + RESPONSE: converted_custom_response, + TRANSITIONS: [{"dst": "dst_test_node", "priority": 1, "cnd": converted_custom_condition}], + PRE_TRANSITION: {}, + } + + +@pytest.fixture +def mapped_flow(info_node): + return {"test_flow": {"1": info_node}} + + +@pytest.fixture +def telegram_interface(): + return {"telegram": {}} + + +@pytest.fixture +def chatsky_telegram_interface(): + return {"chatsky.messengers.TelegramInterface": {"token": {"external:os.getenv": "TG_BOT_TOKEN"}}} diff --git a/backend/chatsky_ui/tests/unit/test_flow_converter.py b/backend/chatsky_ui/tests/unit/test_flow_converter.py new file mode 100644 index 00000000..2d87d325 --- /dev/null +++ b/backend/chatsky_ui/tests/unit/test_flow_converter.py @@ -0,0 +1,115 @@ +import os +from pathlib import Path + +import pytest +import yaml + +from chatsky_ui.services.json_converter.flow_converter import FlowConverter +from chatsky_ui.services.json_converter.interface_converter import InterfaceConverter +from chatsky_ui.services.json_converter.pipeline_converter import PipelineConverter +from chatsky_ui.services.json_converter.script_converter import ScriptConverter + + +@pytest.fixture +def chatsky_flow(chatsky_node): + return {"test_flow": {"test_node": chatsky_node}} + + +class TestFlowConverter: + def test_flow_converter(self, flow, mapped_flow, slots_conf, chatsky_flow): + converted_flow = FlowConverter(flow)(mapped_flows=mapped_flow, slots_conf=slots_conf) + + assert converted_flow == chatsky_flow + + def test_flow_converter_fail_no_nodes(self, flow, mapped_flow, slots_conf): + del flow["data"]["nodes"] + with pytest.raises(ValueError): + FlowConverter(flow) + + def test_flow_converter_fail_no_edges(self, flow, mapped_flow, slots_conf): + del flow["data"]["edges"] + + with pytest.raises(ValueError): + FlowConverter(flow) + + +class TestScriptConverter: + def test_script_converter(self, flow, slots_conf, chatsky_flow): + converted_script = ScriptConverter([flow])(slots_conf=slots_conf) + + assert converted_script == chatsky_flow + + def test_extract_start_fallback_labels(self, flow, slots_conf): + converter = ScriptConverter([flow]) + converter(slots_conf=slots_conf) + + start, fallback = converter.extract_start_fallback_labels() + + assert start + assert fallback + + def test_extract_start_fallback_labels_fail_no_labels(self, flow, slots_conf): + flow["data"]["nodes"][0]["data"]["flags"] = [] + converter = ScriptConverter([flow]) + converter(slots_conf=slots_conf) + + start, fallback = converter.extract_start_fallback_labels() + + assert not start + assert not fallback + + def test_extract_start_fallback_labels_fail_multiple_labels(self, flow, slots_conf): + flow["data"]["nodes"][0]["data"]["flags"] = ["start"] + flow["data"]["nodes"][1]["data"]["flags"] = ["start"] + converter = ScriptConverter([flow]) + converter(slots_conf=slots_conf) + + with pytest.raises(ValueError): + converter.extract_start_fallback_labels() + + +class TestInterfaceConverter: + def test_interface_converter(self, telegram_interface, chatsky_telegram_interface): + os.environ["TG_BOT_TOKEN"] = "some_token" + + converted_interface = InterfaceConverter(telegram_interface)() + + assert converted_interface == chatsky_telegram_interface + + def test_interface_fail_no_token(self, telegram_interface): + os.environ.pop("TG_BOT_TOKEN", None) + with pytest.raises(ValueError): + InterfaceConverter(telegram_interface)() + + def test_interface_fail_multiple_interfaces(self, telegram_interface): + interface = {**telegram_interface, "http": {}} + + with pytest.raises(ValueError): + InterfaceConverter(interface)() + + +class TestPipelineConverter: + def test_pipeline_converter( + self, flow, telegram_interface, chatsky_telegram_interface, converted_group_slot, chatsky_flow + ): + pipeline = {"flows": [flow], "interface": telegram_interface} + pipeline_path = Path(__file__).parent / "test_pipeline.yaml" + with open(pipeline_path, "w") as file: + yaml.dump(pipeline, file) + os.environ["TG_BOT_TOKEN"] = "some_token" + + PipelineConverter()(pipeline_path, Path(__file__).parent) + + output_file = Path(__file__).parent / "build.yaml" + with open(output_file) as file: + converted_pipeline = yaml.load(file, Loader=yaml.Loader) + output_file.unlink() + pipeline_path.unlink() + + assert converted_pipeline == { + "script": chatsky_flow, + "messenger_interface": chatsky_telegram_interface, + "slots": converted_group_slot, + "start_label": ["test_flow", "test_node"], + "fallback_label": ["test_flow", "test_node"], + } diff --git a/backend/chatsky_ui/tests/unit/test_logic_components.py b/backend/chatsky_ui/tests/unit/test_logic_components.py new file mode 100644 index 00000000..fdd231e6 --- /dev/null +++ b/backend/chatsky_ui/tests/unit/test_logic_components.py @@ -0,0 +1,80 @@ +from pathlib import Path + +import pytest + +from chatsky_ui.services.json_converter.logic_component_converter.condition_converter import ( + BadConditionException, + CustomConditionConverter, + SlotConditionConverter, +) +from chatsky_ui.services.json_converter.logic_component_converter.response_converter import ( + BadResponseException, + CustomResponseConverter, + TextResponseConverter, +) +from chatsky_ui.services.json_converter.logic_component_converter.service_replacer import store_custom_service + + +@pytest.fixture +def slot_condition(): + return {"name": "test_condition", "data": {"slot": "test_slot"}} + + +@pytest.fixture +def text_response(): + return {"name": "test_response", "data": [{"text": "test_text"}]} + + +class TestConditionConverter: + def test_custom_condition_converter(self, custom_condition, converted_custom_condition): + converted_cnd = CustomConditionConverter(custom_condition)() + assert converted_cnd == converted_custom_condition + + def test_custom_condition_converter_fail(self, slot_condition): + with pytest.raises(BadConditionException): + CustomConditionConverter(slot_condition)() + + def test_slot_condition_converter(self, slot_condition, slots_conf): + converted_cnd = SlotConditionConverter(slot_condition)(slots_conf=slots_conf) + assert converted_cnd == {"chatsky.conditions.slots.SlotsExtracted": "test_slot"} + + def test_slot_condition_converter_fail(self, custom_condition): + with pytest.raises(BadConditionException): + SlotConditionConverter(custom_condition)() + + def test_slot_condition_converter_get_pre_transitions(self, slot_condition, slots_conf): + converter = SlotConditionConverter(slot_condition) + converter(slots_conf=slots_conf) + assert converter.get_pre_transitions() == {"test_slot": {"chatsky.processing.slots.Extract": "test_slot"}} + + +class TestResponseConverter: + def test_text_response_converter(self, text_response): + converted_response = TextResponseConverter(text_response)() + assert converted_response == {"chatsky.Message": {"text": "test_text"}} + + def test_text_response_converter_fail(self, custom_response): + with pytest.raises(BadResponseException): + TextResponseConverter(custom_response)() + + def test_custom_response_converter(self, custom_response, converted_custom_response): + converted_response = CustomResponseConverter(custom_response)() + assert converted_response == converted_custom_response + + def test_custom_response_converter_fail(self, text_response): + with pytest.raises(BadResponseException): + CustomResponseConverter(text_response)() + + +def test_store_custom_service(): + current_file_path = Path(__file__).resolve() + service_code = """class test_service(BaseService):\n async def call(self, ctx: + Context) -> Message:\n return Message('Hello')""" + test_file_path = current_file_path.parent / "store_service_test.py" + test_file_path.touch(exist_ok=True) + + try: + store_custom_service(test_file_path, [service_code]) + assert test_file_path.stat().st_size > 0 # Check that the file is not empty + finally: + test_file_path.unlink() # Clean up diff --git a/backend/chatsky_ui/tests/unit/test_node_converer.py b/backend/chatsky_ui/tests/unit/test_node_converer.py new file mode 100644 index 00000000..7bdb9a00 --- /dev/null +++ b/backend/chatsky_ui/tests/unit/test_node_converer.py @@ -0,0 +1,19 @@ +from chatsky_ui.services.json_converter.node_converter import InfoNodeConverter, LinkNodeConverter + + +class TestNodeConverter: + def test_info_node_converter(self, info_node, slots_conf, chatsky_node): + converted_node = InfoNodeConverter(info_node)(slots_conf=slots_conf) + + assert converted_node == chatsky_node + + def test_link_node_converter(self): + link_node = { + "id": "test_link_node", + "data": {"transition": {"target_flow": "test_flow", "target_node": "test_node_id"}}, + } + mapped_flows = {"test_flow": {"test_node_id": {"data": {"name": "test_node"}}}} + + converted_node = LinkNodeConverter(link_node)(mapped_flows=mapped_flows) + + assert converted_node == ["test_flow", "test_node"] diff --git a/backend/chatsky_ui/tests/unit/test_slots_converter.py b/backend/chatsky_ui/tests/unit/test_slots_converter.py new file mode 100644 index 00000000..feca20aa --- /dev/null +++ b/backend/chatsky_ui/tests/unit/test_slots_converter.py @@ -0,0 +1,18 @@ +from chatsky_ui.services.json_converter.slots_converter import GroupSlotConverter, RegexpSlotConverter, SlotsConverter + + +class TestSlotsConverter: + def test_slots_converter(self, flow, converted_group_slot): + converted_slots = SlotsConverter([flow])() + + assert converted_slots == converted_group_slot + + def test_regexp_slot_converter(self, regexp_slot, converted_regexp_slot): + converted_slot = RegexpSlotConverter(regexp_slot)() + + assert converted_slot == converted_regexp_slot + + def test_group_slot_converter(self, group_slot, converted_group_slot): + converted_slot = GroupSlotConverter(group_slot)() + + assert converted_slot == converted_group_slot diff --git a/backend/chatsky_ui/utils/git_cmd.py b/backend/chatsky_ui/utils/git_cmd.py new file mode 100644 index 00000000..5b12111b --- /dev/null +++ b/backend/chatsky_ui/utils/git_cmd.py @@ -0,0 +1,50 @@ +from pathlib import Path + +from git import Repo + +from chatsky_ui.core.logger_config import get_logger + + +def commit_changes(repo, commit_message): + repo.git.add(A=True) + repo.index.commit(commit_message) + + +def get_repo(project_dir: Path): + repo = Repo(project_dir) + assert not repo.bare + return repo + + +def delete_tag(repo: Repo, tag_name: str): + repo.git.tag("-d", tag_name) + + +def save_frontend_graph_to_git(build_id: int, chatsky_ui_repo: Repo): + logger = get_logger(__name__) + + commit_changes(chatsky_ui_repo, f"Save script: {build_id}") + chatsky_ui_repo.create_tag(str(build_id)) + logger.info("Flows saved to git with tag %s", build_id) + + tags = sorted(chatsky_ui_repo.tags, key=lambda t: t.commit.committed_datetime) + if len(tags) < 2: + logger.debug("Only one tag found") + is_changed = True + else: + current_tag = tags[-1] + previous_tag = tags[-2] + diff = chatsky_ui_repo.git.diff(previous_tag.commit, current_tag.commit) + logger.debug("Git diff: %s", diff) + is_changed = bool(diff) + + logger.debug("Is changed: %s", is_changed) + return is_changed + + +def save_built_script_to_git(build_id: int, bot_repo: Repo): + logger = get_logger(__name__) + + commit_changes(bot_repo, f"create build: {build_id}") + bot_repo.create_tag(str(build_id)) + logger.info("Bot saved to git with tag %s", build_id) diff --git a/backend/poetry.lock b/backend/poetry.lock index a8ad839b..47016d9b 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -44,9 +44,6 @@ files = [ {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, ] -[package.dependencies] -typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.9\""} - [[package]] name = "antlr4-python3-runtime" version = "4.9.3" @@ -151,40 +148,9 @@ files = [ {file = "babel-2.15.0.tar.gz", hash = "sha256:8daf0e265d05768bc6c7a314cf1321e9a123afc328cc635c18622a2f30a04413"}, ] -[package.dependencies] -pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""} - [package.extras] dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] -[[package]] -name = "backports-zoneinfo" -version = "0.2.1" -description = "Backport of the standard library zoneinfo module" -optional = false -python-versions = ">=3.6" -files = [ - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc"}, - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722"}, - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546"}, - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win32.whl", hash = "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08"}, - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win32.whl", hash = "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"}, - {file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"}, -] - -[package.extras] -tzdata = ["tzdata"] - [[package]] name = "binaryornot" version = "0.4.4" @@ -447,13 +413,13 @@ files = [ [[package]] name = "chatsky" -version = "1.0.0rc1" +version = "0.9.0.dev1" description = "Chatsky is a free and open-source software stack for creating chatbots, released under the terms of Apache License 2.0." optional = false -python-versions = "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,>=3.8" +python-versions = "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,!=3.8.*,>=3.9" files = [ - {file = "chatsky-1.0.0rc1-py3-none-any.whl", hash = "sha256:cd2ab29aa814d1719d68ad8e2245ced165fa3959143b50e4e4347a0cc9887339"}, - {file = "chatsky-1.0.0rc1.tar.gz", hash = "sha256:e6f19886b5d33c2a3f7f96f06f1965ebec875179683fcdda5ca90e1323973a23"}, + {file = "chatsky-0.9.0.dev1-py3-none-any.whl", hash = "sha256:b49acf9abaf5e12fcdd1f03fd47d8f1b9dc373a4fa2e8ece19961be03152b2e2"}, + {file = "chatsky-0.9.0.dev1.tar.gz", hash = "sha256:a5f7ac9e810095d34788f39f35825799da2857c7698caad1cd12a1c70ef7061c"}, ] [package.dependencies] @@ -477,6 +443,7 @@ redis = ["redis"] sqlite = ["aiosqlite", "sqlalchemy[asyncio]"] stats = ["omegaconf", "opentelemetry-exporter-otlp (>=1.20.0)", "opentelemetry-instrumentation", "requests", "tqdm"] telegram = ["python-telegram-bot[all] (>=21.3,<22.0)"] +web-api = ["fastapi", "uvicorn"] yaml = ["pyyaml"] ydb = ["six", "ydb"] @@ -526,40 +493,127 @@ pyyaml = ">=5.3.1" requests = ">=2.23.0" rich = "*" +[[package]] +name = "coverage" +version = "7.6.1" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, + {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"}, + {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"}, + {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"}, + {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"}, + {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"}, + {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"}, + {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"}, + {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"}, + {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"}, + {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"}, + {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"}, + {file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"}, + {file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"}, + {file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"}, + {file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"}, + {file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"}, + {file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"}, + {file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"}, + {file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"}, + {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"}, + {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + [[package]] name = "cryptography" -version = "43.0.1" +version = "43.0.3" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" files = [ - {file = "cryptography-43.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d"}, - {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062"}, - {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962"}, - {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277"}, - {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a"}, - {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042"}, - {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494"}, - {file = "cryptography-43.0.1-cp37-abi3-win32.whl", hash = "sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2"}, - {file = "cryptography-43.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d"}, - {file = "cryptography-43.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d"}, - {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806"}, - {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85"}, - {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c"}, - {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1"}, - {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa"}, - {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4"}, - {file = "cryptography-43.0.1-cp39-abi3-win32.whl", hash = "sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47"}, - {file = "cryptography-43.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb"}, - {file = "cryptography-43.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ea25acb556320250756e53f9e20a4177515f012c9eaea17eb7587a8c4d8ae034"}, - {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c1332724be35d23a854994ff0b66530119500b6053d0bd3363265f7e5e77288d"}, - {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289"}, - {file = "cryptography-43.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5b43d1ea6b378b54a1dc99dd8a2b5be47658fe9a7ce0a58ff0b55f4b43ef2b84"}, - {file = "cryptography-43.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:88cce104c36870d70c49c7c8fd22885875d950d9ee6ab54df2745f83ba0dc365"}, - {file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9d3cdb25fa98afdd3d0892d132b8d7139e2c087da1712041f6b762e4f807cc96"}, - {file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e710bf40870f4db63c3d7d929aa9e09e4e7ee219e703f949ec4073b4294f6172"}, - {file = "cryptography-43.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7c05650fe8023c5ed0d46793d4b7d7e6cd9c04e68eabe5b0aeea836e37bdcec2"}, - {file = "cryptography-43.0.1.tar.gz", hash = "sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d"}, + {file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18"}, + {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd"}, + {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73"}, + {file = "cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2"}, + {file = "cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd"}, + {file = "cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405"}, + {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16"}, + {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73"}, + {file = "cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995"}, + {file = "cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"}, + {file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"}, ] [package.dependencies] @@ -572,7 +626,7 @@ nox = ["nox"] pep8test = ["check-sdist", "click", "mypy", "ruff"] sdist = ["build"] ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi", "cryptography-vectors (==43.0.1)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] test-randomorder = ["pytest-randomly"] [[package]] @@ -664,6 +718,38 @@ mccabe = ">=0.6.0,<0.7.0" pycodestyle = ">=2.8.0,<2.9.0" pyflakes = ">=2.4.0,<2.5.0" +[[package]] +name = "gitdb" +version = "4.0.11" +description = "Git Object Database" +optional = false +python-versions = ">=3.7" +files = [ + {file = "gitdb-4.0.11-py3-none-any.whl", hash = "sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4"}, + {file = "gitdb-4.0.11.tar.gz", hash = "sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b"}, +] + +[package.dependencies] +smmap = ">=3.0.1,<6" + +[[package]] +name = "gitpython" +version = "3.1.43" +description = "GitPython is a Python library used to interact with Git repositories" +optional = false +python-versions = ">=3.7" +files = [ + {file = "GitPython-3.1.43-py3-none-any.whl", hash = "sha256:eec7ec56b92aad751f9912a73404bc02ba212a23adb2c7098ee668417051a1ff"}, + {file = "GitPython-3.1.43.tar.gz", hash = "sha256:35f314a9f878467f5453cc1fee295c3e18e52f1b99f10f6cf5b1682e968a9e7c"}, +] + +[package.dependencies] +gitdb = ">=4.0.1,<5" + +[package.extras] +doc = ["sphinx (==4.3.2)", "sphinx-autodoc-typehints", "sphinx-rtd-theme", "sphinxcontrib-applehelp (>=1.0.2,<=1.0.4)", "sphinxcontrib-devhelp (==1.0.2)", "sphinxcontrib-htmlhelp (>=2.0.0,<=2.0.1)", "sphinxcontrib-qthelp (==1.0.3)", "sphinxcontrib-serializinghtml (==1.1.5)"] +test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions"] + [[package]] name = "h11" version = "0.14.0" @@ -1358,6 +1444,24 @@ pytest = ">=7.0.0,<9" docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] +[[package]] +name = "pytest-cov" +version = "5.0.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, + {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] + [[package]] name = "pytest-mock" version = "3.14.0" @@ -1422,13 +1526,13 @@ unidecode = ["Unidecode (>=1.1.1)"] [[package]] name = "python-telegram-bot" -version = "21.5" +version = "21.7" description = "We have made you a wrapper you can't refuse" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "python_telegram_bot-21.5-py3-none-any.whl", hash = "sha256:1bbba653477ba164411622b717a0cfe1eb7843da016348e41df97f96c93f578e"}, - {file = "python_telegram_bot-21.5.tar.gz", hash = "sha256:2d679173072cce8d6b49aac2e438d49dbfc01c1a4ef5658828c2a65951ee830b"}, + {file = "python_telegram_bot-21.7-py3-none-any.whl", hash = "sha256:aff1d7245f1b0d4d12d41c9acff74e86d7100713c2204cd02ff17f8d80d18846"}, + {file = "python_telegram_bot-21.7.tar.gz", hash = "sha256:bc8537b77ae02531fc2ad440caafc023fd13f13cf19e592dfa1a9ff84988a012"}, ] [package.dependencies] @@ -1458,13 +1562,13 @@ webhooks = ["tornado (>=6.4,<7.0)"] [[package]] name = "pytz" -version = "2024.1" +version = "2024.2" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" files = [ - {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, - {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, + {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, + {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, ] [[package]] @@ -1562,7 +1666,6 @@ files = [ [package.dependencies] markdown-it-py = ">=2.2.0" pygments = ">=2.13.0,<3.0.0" -typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] @@ -1578,6 +1681,17 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +[[package]] +name = "smmap" +version = "5.0.1" +description = "A pure Python implementation of a sliding window memory map manager" +optional = false +python-versions = ">=3.7" +files = [ + {file = "smmap-5.0.1-py3-none-any.whl", hash = "sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da"}, + {file = "smmap-5.0.1.tar.gz", hash = "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62"}, +] + [[package]] name = "sniffio" version = "1.3.1" @@ -1821,22 +1935,22 @@ files = [ [[package]] name = "tornado" -version = "6.4.1" +version = "6.4.2" description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." optional = false python-versions = ">=3.8" files = [ - {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:163b0aafc8e23d8cdc3c9dfb24c5368af84a81e3364745ccb4427669bf84aec8"}, - {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6d5ce3437e18a2b66fbadb183c1d3364fb03f2be71299e7d10dbeeb69f4b2a14"}, - {file = "tornado-6.4.1-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e20b9113cd7293f164dc46fffb13535266e713cdb87bd2d15ddb336e96cfc4"}, - {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ae50a504a740365267b2a8d1a90c9fbc86b780a39170feca9bcc1787ff80842"}, - {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:613bf4ddf5c7a95509218b149b555621497a6cc0d46ac341b30bd9ec19eac7f3"}, - {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25486eb223babe3eed4b8aecbac33b37e3dd6d776bc730ca14e1bf93888b979f"}, - {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:454db8a7ecfcf2ff6042dde58404164d969b6f5d58b926da15e6b23817950fc4"}, - {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a02a08cc7a9314b006f653ce40483b9b3c12cda222d6a46d4ac63bb6c9057698"}, - {file = "tornado-6.4.1-cp38-abi3-win32.whl", hash = "sha256:d9a566c40b89757c9aa8e6f032bcdb8ca8795d7c1a9762910c722b1635c9de4d"}, - {file = "tornado-6.4.1-cp38-abi3-win_amd64.whl", hash = "sha256:b24b8982ed444378d7f21d563f4180a2de31ced9d8d84443907a0a64da2072e7"}, - {file = "tornado-6.4.1.tar.gz", hash = "sha256:92d3ab53183d8c50f8204a51e6f91d18a15d5ef261e84d452800d4ff6fc504e9"}, + {file = "tornado-6.4.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e828cce1123e9e44ae2a50a9de3055497ab1d0aeb440c5ac23064d9e44880da1"}, + {file = "tornado-6.4.2-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:072ce12ada169c5b00b7d92a99ba089447ccc993ea2143c9ede887e0937aa803"}, + {file = "tornado-6.4.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a017d239bd1bb0919f72af256a970624241f070496635784d9bf0db640d3fec"}, + {file = "tornado-6.4.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c36e62ce8f63409301537222faffcef7dfc5284f27eec227389f2ad11b09d946"}, + {file = "tornado-6.4.2-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bca9eb02196e789c9cb5c3c7c0f04fb447dc2adffd95265b2c7223a8a615ccbf"}, + {file = "tornado-6.4.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:304463bd0772442ff4d0f5149c6f1c2135a1fae045adf070821c6cdc76980634"}, + {file = "tornado-6.4.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:c82c46813ba483a385ab2a99caeaedf92585a1f90defb5693351fa7e4ea0bf73"}, + {file = "tornado-6.4.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:932d195ca9015956fa502c6b56af9eb06106140d844a335590c1ec7f5277d10c"}, + {file = "tornado-6.4.2-cp38-abi3-win32.whl", hash = "sha256:2876cef82e6c5978fde1e0d5b1f919d756968d5b4282418f3146b79b58556482"}, + {file = "tornado-6.4.2-cp38-abi3-win_amd64.whl", hash = "sha256:908b71bf3ff37d81073356a5fadcc660eb10c1476ee6e2725588626ce7e5ca38"}, + {file = "tornado-6.4.2.tar.gz", hash = "sha256:92bad5b4746e9879fd7bf1eb21dce4e3fc5128d71601f80005afa39237ad620b"}, ] [[package]] @@ -1884,13 +1998,13 @@ files = [ [[package]] name = "tzdata" -version = "2024.1" +version = "2024.2" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" files = [ - {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, - {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, + {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, + {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, ] [[package]] @@ -1905,7 +2019,6 @@ files = [ ] [package.dependencies] -"backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""} tzdata = {version = "*", markers = "platform_system == \"Windows\""} [package.extras] @@ -2168,81 +2281,76 @@ files = [ [[package]] name = "wrapt" -version = "1.16.0" +version = "1.17.0" description = "Module for decorators, wrappers and monkey patching." optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, - {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, - {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, - {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, - {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, - {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, - {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, - {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, - {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, - {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, - {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, - {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, - {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, - {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, - {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, - {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, - {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, - {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, - {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, - {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, - {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, - {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, - {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, - {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, - {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, - {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, - {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, - {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, - {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, - {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, - {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"}, - {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"}, - {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"}, - {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40"}, - {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"}, - {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"}, - {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"}, - {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"}, - {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"}, - {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"}, - {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"}, - {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"}, - {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf"}, - {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"}, - {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"}, - {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"}, - {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"}, - {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"}, - {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"}, - {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"}, - {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"}, - {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"}, - {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"}, - {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"}, - {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"}, - {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"}, - {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"}, - {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"}, - {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"}, - {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"}, - {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"}, - {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"}, - {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"}, - {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"}, - {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"}, - {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"}, - {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"}, - {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"}, - {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, - {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, + {file = "wrapt-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a0c23b8319848426f305f9cb0c98a6e32ee68a36264f45948ccf8e7d2b941f8"}, + {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1ca5f060e205f72bec57faae5bd817a1560fcfc4af03f414b08fa29106b7e2d"}, + {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e185ec6060e301a7e5f8461c86fb3640a7beb1a0f0208ffde7a65ec4074931df"}, + {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb90765dd91aed05b53cd7a87bd7f5c188fcd95960914bae0d32c5e7f899719d"}, + {file = "wrapt-1.17.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:879591c2b5ab0a7184258274c42a126b74a2c3d5a329df16d69f9cee07bba6ea"}, + {file = "wrapt-1.17.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fce6fee67c318fdfb7f285c29a82d84782ae2579c0e1b385b7f36c6e8074fffb"}, + {file = "wrapt-1.17.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0698d3a86f68abc894d537887b9bbf84d29bcfbc759e23f4644be27acf6da301"}, + {file = "wrapt-1.17.0-cp310-cp310-win32.whl", hash = "sha256:69d093792dc34a9c4c8a70e4973a3361c7a7578e9cd86961b2bbf38ca71e4e22"}, + {file = "wrapt-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:f28b29dc158ca5d6ac396c8e0a2ef45c4e97bb7e65522bfc04c989e6fe814575"}, + {file = "wrapt-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:74bf625b1b4caaa7bad51d9003f8b07a468a704e0644a700e936c357c17dd45a"}, + {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f2a28eb35cf99d5f5bd12f5dd44a0f41d206db226535b37b0c60e9da162c3ed"}, + {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:81b1289e99cf4bad07c23393ab447e5e96db0ab50974a280f7954b071d41b489"}, + {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f2939cd4a2a52ca32bc0b359015718472d7f6de870760342e7ba295be9ebaf9"}, + {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6a9653131bda68a1f029c52157fd81e11f07d485df55410401f745007bd6d339"}, + {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4e4b4385363de9052dac1a67bfb535c376f3d19c238b5f36bddc95efae15e12d"}, + {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bdf62d25234290db1837875d4dceb2151e4ea7f9fff2ed41c0fde23ed542eb5b"}, + {file = "wrapt-1.17.0-cp311-cp311-win32.whl", hash = "sha256:5d8fd17635b262448ab8f99230fe4dac991af1dabdbb92f7a70a6afac8a7e346"}, + {file = "wrapt-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:92a3d214d5e53cb1db8b015f30d544bc9d3f7179a05feb8f16df713cecc2620a"}, + {file = "wrapt-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:89fc28495896097622c3fc238915c79365dd0ede02f9a82ce436b13bd0ab7569"}, + {file = "wrapt-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:875d240fdbdbe9e11f9831901fb8719da0bd4e6131f83aa9f69b96d18fae7504"}, + {file = "wrapt-1.17.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ed16d95fd142e9c72b6c10b06514ad30e846a0d0917ab406186541fe68b451"}, + {file = "wrapt-1.17.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18b956061b8db634120b58f668592a772e87e2e78bc1f6a906cfcaa0cc7991c1"}, + {file = "wrapt-1.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:daba396199399ccabafbfc509037ac635a6bc18510ad1add8fd16d4739cdd106"}, + {file = "wrapt-1.17.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4d63f4d446e10ad19ed01188d6c1e1bb134cde8c18b0aa2acfd973d41fcc5ada"}, + {file = "wrapt-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8a5e7cc39a45fc430af1aefc4d77ee6bad72c5bcdb1322cfde852c15192b8bd4"}, + {file = "wrapt-1.17.0-cp312-cp312-win32.whl", hash = "sha256:0a0a1a1ec28b641f2a3a2c35cbe86c00051c04fffcfcc577ffcdd707df3f8635"}, + {file = "wrapt-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:3c34f6896a01b84bab196f7119770fd8466c8ae3dfa73c59c0bb281e7b588ce7"}, + {file = "wrapt-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:714c12485aa52efbc0fc0ade1e9ab3a70343db82627f90f2ecbc898fdf0bb181"}, + {file = "wrapt-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da427d311782324a376cacb47c1a4adc43f99fd9d996ffc1b3e8529c4074d393"}, + {file = "wrapt-1.17.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba1739fb38441a27a676f4de4123d3e858e494fac05868b7a281c0a383c098f4"}, + {file = "wrapt-1.17.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e711fc1acc7468463bc084d1b68561e40d1eaa135d8c509a65dd534403d83d7b"}, + {file = "wrapt-1.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:140ea00c87fafc42739bd74a94a5a9003f8e72c27c47cd4f61d8e05e6dec8721"}, + {file = "wrapt-1.17.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:73a96fd11d2b2e77d623a7f26e004cc31f131a365add1ce1ce9a19e55a1eef90"}, + {file = "wrapt-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0b48554952f0f387984da81ccfa73b62e52817a4386d070c75e4db7d43a28c4a"}, + {file = "wrapt-1.17.0-cp313-cp313-win32.whl", hash = "sha256:498fec8da10e3e62edd1e7368f4b24aa362ac0ad931e678332d1b209aec93045"}, + {file = "wrapt-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd136bb85f4568fffca995bd3c8d52080b1e5b225dbf1c2b17b66b4c5fa02838"}, + {file = "wrapt-1.17.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:17fcf043d0b4724858f25b8826c36e08f9fb2e475410bece0ec44a22d533da9b"}, + {file = "wrapt-1.17.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4a557d97f12813dc5e18dad9fa765ae44ddd56a672bb5de4825527c847d6379"}, + {file = "wrapt-1.17.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0229b247b0fc7dee0d36176cbb79dbaf2a9eb7ecc50ec3121f40ef443155fb1d"}, + {file = "wrapt-1.17.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8425cfce27b8b20c9b89d77fb50e368d8306a90bf2b6eef2cdf5cd5083adf83f"}, + {file = "wrapt-1.17.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9c900108df470060174108012de06d45f514aa4ec21a191e7ab42988ff42a86c"}, + {file = "wrapt-1.17.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4e547b447073fc0dbfcbff15154c1be8823d10dab4ad401bdb1575e3fdedff1b"}, + {file = "wrapt-1.17.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:914f66f3b6fc7b915d46c1cc424bc2441841083de01b90f9e81109c9759e43ab"}, + {file = "wrapt-1.17.0-cp313-cp313t-win32.whl", hash = "sha256:a4192b45dff127c7d69b3bdfb4d3e47b64179a0b9900b6351859f3001397dabf"}, + {file = "wrapt-1.17.0-cp313-cp313t-win_amd64.whl", hash = "sha256:4f643df3d4419ea3f856c5c3f40fec1d65ea2e89ec812c83f7767c8730f9827a"}, + {file = "wrapt-1.17.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:69c40d4655e078ede067a7095544bcec5a963566e17503e75a3a3e0fe2803b13"}, + {file = "wrapt-1.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f495b6754358979379f84534f8dd7a43ff8cff2558dcdea4a148a6e713a758f"}, + {file = "wrapt-1.17.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:baa7ef4e0886a6f482e00d1d5bcd37c201b383f1d314643dfb0367169f94f04c"}, + {file = "wrapt-1.17.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fc931382e56627ec4acb01e09ce66e5c03c384ca52606111cee50d931a342d"}, + {file = "wrapt-1.17.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8f8909cdb9f1b237786c09a810e24ee5e15ef17019f7cecb207ce205b9b5fcce"}, + {file = "wrapt-1.17.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ad47b095f0bdc5585bced35bd088cbfe4177236c7df9984b3cc46b391cc60627"}, + {file = "wrapt-1.17.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:948a9bd0fb2c5120457b07e59c8d7210cbc8703243225dbd78f4dfc13c8d2d1f"}, + {file = "wrapt-1.17.0-cp38-cp38-win32.whl", hash = "sha256:5ae271862b2142f4bc687bdbfcc942e2473a89999a54231aa1c2c676e28f29ea"}, + {file = "wrapt-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:f335579a1b485c834849e9075191c9898e0731af45705c2ebf70e0cd5d58beed"}, + {file = "wrapt-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d751300b94e35b6016d4b1e7d0e7bbc3b5e1751e2405ef908316c2a9024008a1"}, + {file = "wrapt-1.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7264cbb4a18dc4acfd73b63e4bcfec9c9802614572025bdd44d0721983fc1d9c"}, + {file = "wrapt-1.17.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33539c6f5b96cf0b1105a0ff4cf5db9332e773bb521cc804a90e58dc49b10578"}, + {file = "wrapt-1.17.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c30970bdee1cad6a8da2044febd824ef6dc4cc0b19e39af3085c763fdec7de33"}, + {file = "wrapt-1.17.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bc7f729a72b16ee21795a943f85c6244971724819819a41ddbaeb691b2dd85ad"}, + {file = "wrapt-1.17.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:6ff02a91c4fc9b6a94e1c9c20f62ea06a7e375f42fe57587f004d1078ac86ca9"}, + {file = "wrapt-1.17.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2dfb7cff84e72e7bf975b06b4989477873dcf160b2fd89959c629535df53d4e0"}, + {file = "wrapt-1.17.0-cp39-cp39-win32.whl", hash = "sha256:2399408ac33ffd5b200480ee858baa58d77dd30e0dd0cab6a8a9547135f30a88"}, + {file = "wrapt-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:4f763a29ee6a20c529496a20a7bcb16a73de27f5da6a843249c7047daf135977"}, + {file = "wrapt-1.17.0-py3-none-any.whl", hash = "sha256:d2c63b93548eda58abf5188e505ffed0229bf675f7c3090f8e36ad55b8cbc371"}, + {file = "wrapt-1.17.0.tar.gz", hash = "sha256:16187aa2317c731170a88ef35e8937ae0f533c402872c1ee5e6d079fcf320801"}, ] [[package]] @@ -2280,5 +2388,5 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" -python-versions = "^3.8.1" -content-hash = "c74907728cefa15f4c599238fa17b5f174afa6f54e483e75e9a4388265462f51" +python-versions = "^3.9,!=3.9.7" +content-hash = "a676df6948689fe5ff0e011484f12ecdde34add2dd2dfca1d290f1d8f8203674" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 4d7017f1..a089a25a 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "chatsky-ui" -version = "0.3.0" +version = "0.4.0" description = "Chatsky-UI is GUI for Chatsky Framework, that is a free and open-source software stack for creating chatbots, released under the terms of Apache License 2.0." license = "Apache-2.0" authors = [ @@ -12,7 +12,7 @@ readme = "README.md" packages = [{include = "chatsky_ui"}] [tool.poetry.dependencies] -python = "^3.8.1" +python = "^3.9,!=3.9.7" fastapi = "^0.110.0" uvicorn = {extras = ["standard"], version = "^0.28.0"} pydantic = "^2.6.3" @@ -20,16 +20,12 @@ typer = "^0.9.0" pydantic-settings = "^2.2.1" aiofiles = "^23.2.1" cookiecutter = "^2.6.0" -chatsky = {version = "1.0.0rc1", extras = ["yaml", "telegram"]} +chatsky = {extras = ["yaml", "telegram"], version = "==0.9.0.dev1"} omegaconf = "^2.3.0" -pytest = "^8.1.1" -pytest-asyncio = "^0.23.6" -pytest-mock = "^3.14.0" httpx = "^0.27.0" httpx-ws = "^0.6.0" pylint = "^3.2.3" -sphinx = "*" -sphinx-rtd-theme = "*" +gitpython = "^3.1.43" [tool.poetry.scripts] "chatsky.ui" = "chatsky_ui.cli:cli" @@ -41,3 +37,14 @@ optional = true isort = "^5" black = "^22" flake8 = "^4" + +[tool.poetry.group.dev] +optional = true + +[tool.poetry.group.dev.dependencies] +pytest = "^8.1.1" +pytest-asyncio = "^0.23.6" +pytest-mock = "^3.14.0" +sphinx = "*" +sphinx-rtd-theme = "*" +pytest-cov = "^5.0.0" diff --git a/bin/add_ui_to_toml.sh b/bin/add_ui_to_toml.sh deleted file mode 100755 index a0be2353..00000000 --- a/bin/add_ui_to_toml.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -# Find the latest version of the wheel file -VERSION=$(basename $(ls ../backend/dist/chatsky_ui-*.whl) | sed -E 's/chatsky_ui-([^-]+)-.*/\1/' | head -n 1) - -# Add the specific version to my project -poetry add ../backend/dist/chatsky_ui-$VERSION-py3-none-any.whl diff --git a/docs/appref/chatsky_ui/services.rst b/docs/appref/chatsky_ui/services.rst index 3be7e826..7b3c0074 100644 --- a/docs/appref/chatsky_ui/services.rst +++ b/docs/appref/chatsky_ui/services.rst @@ -1,14 +1,6 @@ chatsky_ui.services package ==================== -chatsky_ui.services.index module -------------------------- - -.. automodule:: chatsky_ui.services.index - :members: - :undoc-members: - :show-inheritance: - chatsky_ui.services.json\_converter module ----------------------------------- diff --git a/frontend/src/modals/ConditionModal/components/SlotCondition.tsx b/frontend/src/modals/ConditionModal/components/SlotCondition.tsx index 61560ba2..21d32fc4 100644 --- a/frontend/src/modals/ConditionModal/components/SlotCondition.tsx +++ b/frontend/src/modals/ConditionModal/components/SlotCondition.tsx @@ -103,6 +103,7 @@ const SlotCondition = ({ condition, setData }: ConditionModalContentType) => { value: slot.name, }))} placeholder="Choose slot" + onValueChange={(value) => setSelectedSlot(value)} />