mirror of
https://github.com/PaiGramTeam/PaiGram.git
synced 2024-11-21 22:58:05 +00:00
✨ Add inline search
This commit is contained in:
parent
bfab1ec213
commit
e20e20111d
10
core/search/__init__.py
Normal file
10
core/search/__init__.py
Normal file
@ -0,0 +1,10 @@
|
||||
from core.service import init_service
|
||||
from .services import SearchServices as _SearchServices
|
||||
|
||||
__all__ = []
|
||||
|
||||
|
||||
@init_service
|
||||
def create_search_service():
|
||||
_service = _SearchServices()
|
||||
return _service
|
73
core/search/models.py
Normal file
73
core/search/models.py
Normal file
@ -0,0 +1,73 @@
|
||||
from abc import abstractmethod
|
||||
from typing import Optional, List
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
__all__ = ["BaseEntry", "WeaponEntry", "WeaponsEntry", "StrategyEntry", "StrategyEntryList"]
|
||||
|
||||
from thefuzz import fuzz
|
||||
|
||||
|
||||
class BaseEntry(BaseModel):
|
||||
"""所有可搜索条目的基类。
|
||||
|
||||
Base class for all searchable entries."""
|
||||
|
||||
key: str # 每个条目的Key必须唯一
|
||||
title: str
|
||||
description: str
|
||||
tags: Optional[List[str]] = []
|
||||
caption: Optional[str] = None
|
||||
parse_mode: Optional[str] = None
|
||||
photo_url: Optional[str] = None
|
||||
photo_file_id: Optional[str] = None
|
||||
|
||||
@abstractmethod
|
||||
def compare_to_query(self, search_query: str) -> float:
|
||||
"""返回一个数字 ∈[0,100] 描述搜索查询与此条目的相似程度。
|
||||
|
||||
Gives a number ∈[0,100] describing how similar the search query is to this entry."""
|
||||
|
||||
|
||||
class WeaponEntry(BaseEntry):
|
||||
def compare_to_query(self, search_query: str) -> float:
|
||||
score = 0.0
|
||||
if search_query == self.title:
|
||||
return 100
|
||||
if self.tags:
|
||||
if search_query in self.tags:
|
||||
return 99
|
||||
for tag in self.tags:
|
||||
_score = fuzz.partial_token_set_ratio(tag, search_query)
|
||||
if _score >= score:
|
||||
score = _score
|
||||
if score >= 90:
|
||||
return score * 0.99
|
||||
if self.description:
|
||||
_score = fuzz.partial_token_set_ratio(self.description, search_query)
|
||||
if _score >= score:
|
||||
return _score
|
||||
return score
|
||||
|
||||
|
||||
class WeaponsEntry(BaseModel):
|
||||
data: Optional[List[WeaponEntry]]
|
||||
|
||||
|
||||
class StrategyEntry(BaseEntry):
|
||||
def compare_to_query(self, search_query: str) -> float:
|
||||
score = 0.0
|
||||
if search_query == self.title:
|
||||
return 100
|
||||
if self.tags:
|
||||
if search_query in self.tags:
|
||||
return 99
|
||||
for tag in self.tags:
|
||||
_score = fuzz.partial_token_set_ratio(tag, search_query)
|
||||
if _score >= score:
|
||||
score = _score
|
||||
return score
|
||||
|
||||
|
||||
class StrategyEntryList(BaseModel):
|
||||
data: Optional[List[StrategyEntry]]
|
148
core/search/services.py
Normal file
148
core/search/services.py
Normal file
@ -0,0 +1,148 @@
|
||||
import asyncio
|
||||
import heapq
|
||||
import itertools
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Tuple, List, Optional, Dict
|
||||
|
||||
import aiofiles
|
||||
from async_lru import alru_cache
|
||||
|
||||
from core.search.models import WeaponEntry, BaseEntry, WeaponsEntry, StrategyEntry, StrategyEntryList
|
||||
from utils.const import PROJECT_ROOT
|
||||
|
||||
ENTRY_DAYA_PATH = PROJECT_ROOT.joinpath("data", "entry")
|
||||
ENTRY_DAYA_PATH.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
class SearchServices:
|
||||
def __init__(self):
|
||||
self._lock = asyncio.Lock() # 访问和修改操作成员变量必须加锁操作
|
||||
self.weapons: List[WeaponEntry] = []
|
||||
self.strategy: List[StrategyEntry] = []
|
||||
self.entry_data_path: Path = ENTRY_DAYA_PATH
|
||||
self.weapons_entry_data_path = self.entry_data_path / "weapon.json"
|
||||
self.strategy_entry_data_path = self.entry_data_path / "strategy.json"
|
||||
self.replace_time: Dict[str, float] = {}
|
||||
|
||||
@staticmethod
|
||||
async def load_json(path):
|
||||
async with aiofiles.open(path, "r", encoding="utf-8") as f:
|
||||
return json.loads(await f.read())
|
||||
|
||||
@staticmethod
|
||||
async def save_json(path, data):
|
||||
async with aiofiles.open(path, "w", encoding="utf-8") as f:
|
||||
await f.write(data)
|
||||
|
||||
async def load_data(self):
|
||||
async with self._lock:
|
||||
if self.weapons_entry_data_path.exists():
|
||||
weapon_json = await self.load_json(self.weapons_entry_data_path)
|
||||
weapons = WeaponsEntry.parse_obj(weapon_json)
|
||||
for weapon in weapons.data:
|
||||
self.weapons.append(weapon.copy())
|
||||
if self.strategy_entry_data_path.exists():
|
||||
strategy_json = await self.load_json(self.strategy_entry_data_path)
|
||||
strategy = StrategyEntryList.parse_obj(strategy_json)
|
||||
for strategy in strategy.data:
|
||||
self.strategy.append(strategy.copy())
|
||||
|
||||
async def save_entry(self) -> None:
|
||||
"""保存条目
|
||||
:return: None
|
||||
"""
|
||||
async with self._lock:
|
||||
if len(self.weapons) > 0:
|
||||
weapons = WeaponsEntry(data=self.weapons)
|
||||
await self.save_json(self.weapons_entry_data_path, weapons.json())
|
||||
if len(self.strategy) > 0:
|
||||
strategy = StrategyEntryList(data=self.strategy)
|
||||
await self.save_json(self.strategy_entry_data_path, strategy.json())
|
||||
|
||||
async def add_entry(self, entry: BaseEntry, update: bool = False, ttl: int = 3600):
|
||||
"""添加条目
|
||||
:param entry: 条目数据
|
||||
:param update: 如果条目存在是否覆盖
|
||||
:param ttl: 条目存在时需要多久时间覆盖
|
||||
:return: None
|
||||
"""
|
||||
async with self._lock:
|
||||
replace_time = self.replace_time.get(entry.key)
|
||||
if replace_time and replace_time <= time.time() + ttl:
|
||||
return
|
||||
if isinstance(entry, WeaponEntry):
|
||||
for index, value in enumerate(self.weapons):
|
||||
if value.key == entry.key:
|
||||
if update:
|
||||
self.replace_time[entry.key] = time.time()
|
||||
self.weapons[index] = entry
|
||||
break
|
||||
else:
|
||||
self.weapons.append(entry)
|
||||
elif isinstance(entry, StrategyEntry):
|
||||
for index, value in enumerate(self.strategy):
|
||||
if value.key == entry.key:
|
||||
if update:
|
||||
self.replace_time[entry.key] = time.time()
|
||||
self.strategy[index] = entry
|
||||
break
|
||||
else:
|
||||
self.strategy.append(entry)
|
||||
|
||||
async def remove_all_entry(self):
|
||||
"""移除全部条目
|
||||
:return: None
|
||||
"""
|
||||
async with self._lock:
|
||||
self.weapons = []
|
||||
if self.weapons_entry_data_path.exists():
|
||||
os.remove(self.weapons_entry_data_path)
|
||||
self.strategy = []
|
||||
if self.strategy_entry_data_path.exists():
|
||||
os.remove(self.strategy_entry_data_path)
|
||||
|
||||
@staticmethod
|
||||
def _sort_key(entry: BaseEntry, search_query: str) -> float:
|
||||
return entry.compare_to_query(search_query)
|
||||
|
||||
@alru_cache(maxsize=64)
|
||||
async def multi_search_combinations(self, search_queries: Tuple[str], results_per_query: int = 3):
|
||||
"""多个关键词搜索
|
||||
:param search_queries: 搜索文本
|
||||
:param results_per_query: 约定返回的数目
|
||||
:return: 搜索结果
|
||||
"""
|
||||
results = {}
|
||||
effective_queries = list(dict.fromkeys(search_queries))
|
||||
for query in effective_queries:
|
||||
if res := await self.search(search_query=query, amount=results_per_query):
|
||||
results[query] = res
|
||||
|
||||
@alru_cache(maxsize=64)
|
||||
async def search(self, search_query: Optional[str], amount: int = None) -> Optional[List[BaseEntry]]:
|
||||
"""在所有可用条目中搜索适当的结果
|
||||
:param search_query: 搜索文本
|
||||
:param amount: 约定返回的数目
|
||||
:return: 搜索结果
|
||||
"""
|
||||
# search_entries: Iterable[BaseEntry] = []
|
||||
async with self._lock:
|
||||
search_entries = itertools.chain(self.weapons, self.strategy)
|
||||
|
||||
if not search_query:
|
||||
return search_entries if isinstance(search_entries, list) else list(search_entries)
|
||||
|
||||
if not amount:
|
||||
return sorted(
|
||||
search_entries,
|
||||
key=lambda entry: self._sort_key(entry, search_query), # type: ignore
|
||||
reverse=True,
|
||||
)
|
||||
return heapq.nlargest(
|
||||
amount,
|
||||
search_entries,
|
||||
key=lambda entry: self._sort_key(entry, search_query), # type: ignore[arg-type]
|
||||
)
|
@ -1,10 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
from typing import List
|
||||
|
||||
from metadata.genshin import WEAPON_DATA
|
||||
|
||||
__all__ = ["roles", "weapons", "roleToId", "roleToName", "weaponToName", "weaponToId", "not_real_roles"]
|
||||
__all__ = ["roles", "weapons", "roleToId", "roleToName", "weaponToName", "weaponToId", "not_real_roles", "roleToTag"]
|
||||
|
||||
# noinspection SpellCheckingInspection
|
||||
roles = {
|
||||
@ -393,3 +394,10 @@ def weaponToName(shortname: str) -> str:
|
||||
def weaponToId(name: str) -> int | None:
|
||||
"""获取武器ID"""
|
||||
return next((int(key) for key, value in WEAPON_DATA.items() if weaponToName(name) in value["name"]), None)
|
||||
|
||||
|
||||
# noinspection PyPep8Naming
|
||||
@functools.lru_cache()
|
||||
def roleToTag(role_name: str) -> List[str]:
|
||||
"""通过角色名获取TAG"""
|
||||
return next((value for value in roles.values() if value[0] == role_name), [role_name])
|
||||
|
@ -8,7 +8,9 @@ from telegram.ext import MessageHandler, filters
|
||||
from core.baseplugin import BasePlugin
|
||||
from core.game.services import GameStrategyService
|
||||
from core.plugin import Plugin, handler
|
||||
from metadata.shortname import roleToName
|
||||
from core.search.models import StrategyEntry
|
||||
from core.search.services import SearchServices
|
||||
from metadata.shortname import roleToName, roleToTag
|
||||
from utils.bot import get_args
|
||||
from utils.decorators.error import error_callable
|
||||
from utils.decorators.restricts import restricts
|
||||
@ -21,8 +23,13 @@ class StrategyPlugin(Plugin, BasePlugin):
|
||||
|
||||
KEYBOARD = [[InlineKeyboardButton(text="查看角色攻略列表并查询", switch_inline_query_current_chat="查看角色攻略列表并查询")]]
|
||||
|
||||
def __init__(self, game_strategy_service: GameStrategyService = None):
|
||||
def __init__(
|
||||
self,
|
||||
game_strategy_service: GameStrategyService = None,
|
||||
search_service: SearchServices = None,
|
||||
):
|
||||
self.game_strategy_service = game_strategy_service
|
||||
self.search_service = search_service
|
||||
|
||||
@handler(CommandHandler, command="strategy", block=False)
|
||||
@handler(MessageHandler, filters=filters.Regex("^角色攻略查询(.*)"), block=False)
|
||||
@ -53,11 +60,24 @@ class StrategyPlugin(Plugin, BasePlugin):
|
||||
logger.info(f"用户 {user.full_name}[{user.id}] 查询角色攻略命令请求 || 参数 {character_name}")
|
||||
await message.reply_chat_action(ChatAction.UPLOAD_PHOTO)
|
||||
file_path = await url_to_file(url, return_path=True)
|
||||
caption = "From 米游社 西风驿站 " f"查看 [原图]({url})"
|
||||
await message.reply_photo(
|
||||
caption = f"From 米游社 西风驿站 查看<a href='{url}'>原图</a>"
|
||||
reply_photo = await message.reply_photo(
|
||||
photo=open(file_path, "rb"),
|
||||
caption=caption,
|
||||
filename=f"{character_name}.png",
|
||||
allow_sending_without_reply=True,
|
||||
parse_mode=ParseMode.MARKDOWN_V2,
|
||||
parse_mode=ParseMode.HTML,
|
||||
)
|
||||
if reply_photo.photo:
|
||||
tags = roleToTag(character_name)
|
||||
photo_file_id = reply_photo.photo[0].file_id
|
||||
entry = StrategyEntry(
|
||||
key=f"plugin:strategy:{character_name}",
|
||||
title=character_name,
|
||||
description=f"{character_name} 角色攻略",
|
||||
tags=tags,
|
||||
caption=caption,
|
||||
parse_mode="HTML",
|
||||
photo_file_id=photo_file_id,
|
||||
)
|
||||
await self.search_service.add_entry(entry)
|
||||
|
@ -5,10 +5,12 @@ from telegram.ext import CallbackContext, CommandHandler, MessageHandler, filter
|
||||
from core.base.assets import AssetsService, AssetsCouldNotFound
|
||||
from core.baseplugin import BasePlugin
|
||||
from core.plugin import Plugin, handler
|
||||
from core.search.models import WeaponEntry
|
||||
from core.search.services import SearchServices
|
||||
from core.template import TemplateService
|
||||
from core.wiki.services import WikiService
|
||||
from metadata.genshin import honey_id_to_game_id
|
||||
from metadata.shortname import weaponToName
|
||||
from metadata.shortname import weaponToName, weapons as _weapons_data
|
||||
from modules.wiki.weapon import Weapon
|
||||
from utils.bot import get_args
|
||||
from utils.decorators.error import error_callable
|
||||
@ -27,10 +29,12 @@ class WeaponPlugin(Plugin, BasePlugin):
|
||||
template_service: TemplateService = None,
|
||||
wiki_service: WikiService = None,
|
||||
assets_service: AssetsService = None,
|
||||
search_service: SearchServices = None,
|
||||
):
|
||||
self.wiki_service = wiki_service
|
||||
self.template_service = template_service
|
||||
self.assets_service = assets_service
|
||||
self.search_service = search_service
|
||||
|
||||
@handler(CommandHandler, command="weapon", block=False)
|
||||
@handler(MessageHandler, filters=filters.Regex("^武器查询(.*)"), block=False)
|
||||
@ -122,8 +126,19 @@ class WeaponPlugin(Plugin, BasePlugin):
|
||||
"genshin/weapon/weapon.html", template_data, {"width": 540, "height": 540}, ttl=31 * 24 * 60 * 60
|
||||
)
|
||||
await message.reply_chat_action(ChatAction.UPLOAD_PHOTO)
|
||||
await png_data.reply_photo(
|
||||
reply_photo = await png_data.reply_photo(
|
||||
message,
|
||||
filename=f"{template_data['weapon_name']}.png",
|
||||
allow_sending_without_reply=True,
|
||||
)
|
||||
if reply_photo.photo:
|
||||
photo_file_id = reply_photo.photo[0].file_id
|
||||
tags = _weapons_data.get(weapon_name)
|
||||
entry = WeaponEntry(
|
||||
key=f"plugin:weapon:{weapon_name}",
|
||||
title=weapon_name,
|
||||
description=weapon_data.story,
|
||||
tags=tags,
|
||||
photo_file_id=photo_file_id,
|
||||
)
|
||||
await self.search_service.add_entry(entry)
|
||||
|
@ -2,13 +2,20 @@ import asyncio
|
||||
from typing import cast, Dict, Awaitable, List
|
||||
from uuid import uuid4
|
||||
|
||||
from telegram import InlineQueryResultArticle, InputTextMessageContent, Update, InlineQuery
|
||||
from telegram import (
|
||||
InlineQueryResultArticle,
|
||||
InputTextMessageContent,
|
||||
Update,
|
||||
InlineQuery,
|
||||
InlineQueryResultCachedPhoto,
|
||||
)
|
||||
from telegram.constants import ParseMode
|
||||
from telegram.error import BadRequest
|
||||
from telegram.ext import CallbackContext, InlineQueryHandler
|
||||
|
||||
from core.base.assets import AssetsService, AssetsCouldNotFound
|
||||
from core.plugin import handler, Plugin
|
||||
from core.search.services import SearchServices
|
||||
from core.wiki import WikiService
|
||||
from utils.decorators.error import error_callable
|
||||
from utils.log import logger
|
||||
@ -21,12 +28,14 @@ class Inline(Plugin):
|
||||
self,
|
||||
wiki_service: WikiService = None,
|
||||
assets_service: AssetsService = None,
|
||||
search_service: SearchServices = None,
|
||||
):
|
||||
self.assets_service = assets_service
|
||||
self.wiki_service = wiki_service
|
||||
self.weapons_list: List[Dict[str, str]] = []
|
||||
self.characters_list: List[Dict[str, str]] = []
|
||||
self.refresh_task: List[Awaitable] = []
|
||||
self.search_service = search_service
|
||||
|
||||
async def __async_init__(self):
|
||||
# todo: 整合进 wiki 或者单独模块 从Redis中读取
|
||||
@ -74,7 +83,22 @@ class Inline(Plugin):
|
||||
results_list = []
|
||||
args = query.split(" ")
|
||||
if args[0] == "":
|
||||
pass
|
||||
results_list.append(
|
||||
InlineQueryResultArticle(
|
||||
id=str(uuid4()),
|
||||
title="武器图鉴查询",
|
||||
description="输入武器名称即可查询武器图鉴",
|
||||
input_message_content=InputTextMessageContent("武器图鉴查询"),
|
||||
)
|
||||
)
|
||||
results_list.append(
|
||||
InlineQueryResultArticle(
|
||||
id=str(uuid4()),
|
||||
title="角色攻略查询",
|
||||
description="输入角色名即可查询角色攻略",
|
||||
input_message_content=InputTextMessageContent("角色攻略查询"),
|
||||
)
|
||||
)
|
||||
else:
|
||||
if "查看武器列表并查询" == args[0]:
|
||||
for weapon in self.weapons_list:
|
||||
@ -119,6 +143,33 @@ class Inline(Plugin):
|
||||
),
|
||||
)
|
||||
)
|
||||
else:
|
||||
simple_search_results = await self.search_service.search(args[0])
|
||||
if simple_search_results:
|
||||
results_list.append(
|
||||
InlineQueryResultArticle(
|
||||
id=str(uuid4()),
|
||||
title=f"当前查询内容为 {args[0]}",
|
||||
description="如果无查看图片描述 这是正常的 客户端问题",
|
||||
thumb_url="https://www.miyoushe.com/_nuxt/img/game-ys.dfc535b.jpg",
|
||||
input_message_content=InputTextMessageContent(f"当前查询内容为 {args[0]}\n如果无查看图片描述 这是正常的 客户端问题"),
|
||||
)
|
||||
)
|
||||
for simple_search_result in simple_search_results:
|
||||
if simple_search_result.photo_file_id:
|
||||
description = simple_search_result.description
|
||||
if len(description) >= 10:
|
||||
description = description[:10]
|
||||
results_list.append(
|
||||
InlineQueryResultCachedPhoto(
|
||||
id=str(uuid4()),
|
||||
title=simple_search_result.title,
|
||||
photo_file_id=simple_search_result.photo_file_id,
|
||||
description=description,
|
||||
caption=simple_search_result.caption,
|
||||
parse_mode=simple_search_result.parse_mode,
|
||||
)
|
||||
)
|
||||
|
||||
if not results_list:
|
||||
results_list.append(
|
||||
|
64
plugins/system/search.py
Normal file
64
plugins/system/search.py
Normal file
@ -0,0 +1,64 @@
|
||||
import asyncio
|
||||
import datetime
|
||||
|
||||
from telegram import Update
|
||||
from telegram.ext import CallbackContext
|
||||
|
||||
from core.plugin import handler, Plugin, job
|
||||
from core.search.services import SearchServices
|
||||
from utils.decorators.admins import bot_admins_rights_check
|
||||
from utils.decorators.restricts import restricts
|
||||
from utils.log import logger
|
||||
|
||||
__all__ = []
|
||||
|
||||
|
||||
class SearchPlugin(Plugin):
|
||||
def __init__(self, search: SearchServices = None):
|
||||
self.search = search
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
async def __async_init__(self):
|
||||
async def load_data():
|
||||
logger.info("Search 插件模块正在加载搜索条目")
|
||||
async with self._lock:
|
||||
await self.search.load_data()
|
||||
logger.success("Search 插件加载模块搜索条目成功")
|
||||
|
||||
asyncio.create_task(load_data())
|
||||
|
||||
@job.run_repeating(interval=datetime.timedelta(hours=1), name="SaveEntryJob")
|
||||
async def save_entry_job(self, _: CallbackContext):
|
||||
if self._lock.locked():
|
||||
logger.warning("条目数据正在保存 跳过本次定时任务")
|
||||
else:
|
||||
async with self._lock:
|
||||
logger.info("条目数据正在自动保存")
|
||||
await self.search.save_entry()
|
||||
logger.success("条目数据自动保存成功")
|
||||
|
||||
@handler.command("save_entry", block=False)
|
||||
@bot_admins_rights_check
|
||||
@restricts()
|
||||
async def save_entry(self, update: Update, _: CallbackContext):
|
||||
user = update.effective_user
|
||||
message = update.effective_message
|
||||
logger.info("用户 %s[%s] 保存条目数据命令请求", user.full_name, user.id)
|
||||
if self._lock.locked():
|
||||
await message.reply_text("条目数据正在保存 请稍后重试")
|
||||
else:
|
||||
async with self._lock:
|
||||
reply_text = await message.reply_text("正在保存数据")
|
||||
await self.search.save_entry()
|
||||
await reply_text.edit_text("数据保存成功")
|
||||
|
||||
@handler.command("remove_all_entry", block=False)
|
||||
@bot_admins_rights_check
|
||||
@restricts()
|
||||
async def remove_all_entry(self, update: Update, _: CallbackContext):
|
||||
user = update.effective_user
|
||||
message = update.effective_message
|
||||
logger.info("用户 %s[%s] 删除全部条目数据命令请求", user.full_name, user.id)
|
||||
reply_text = await message.reply_text("正在删除全部条目数据")
|
||||
await self.search.remove_all_entry()
|
||||
await reply_text.edit_text("删除全部条目数据成功")
|
51
poetry.lock
generated
51
poetry.lock
generated
@ -131,6 +131,14 @@ typing-extensions = "*"
|
||||
[package.extras]
|
||||
test = ["pytest", "pytest-rerunfailures"]
|
||||
|
||||
[[package]]
|
||||
name = "async-lru"
|
||||
version = "1.0.3"
|
||||
description = "Simple lru_cache for asyncio"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
name = "async-timeout"
|
||||
version = "4.0.2"
|
||||
@ -159,7 +167,7 @@ python-versions = ">=3.5"
|
||||
dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"]
|
||||
docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"]
|
||||
tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"]
|
||||
tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"]
|
||||
tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"]
|
||||
|
||||
[[package]]
|
||||
name = "backports.zoneinfo"
|
||||
@ -234,7 +242,7 @@ optional = false
|
||||
python-versions = ">=3.6.0"
|
||||
|
||||
[package.extras]
|
||||
unicode-backport = ["unicodedata2"]
|
||||
unicode_backport = ["unicodedata2"]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
@ -951,7 +959,7 @@ falcon = ["falcon (>=1.4)"]
|
||||
fastapi = ["fastapi (>=0.79.0)"]
|
||||
flask = ["blinker (>=1.1)", "flask (>=0.11)"]
|
||||
httpx = ["httpx (>=0.16.0)"]
|
||||
pure-eval = ["asttokens", "executing", "pure-eval"]
|
||||
pure_eval = ["asttokens", "executing", "pure-eval"]
|
||||
pymongo = ["pymongo (>=3.1)"]
|
||||
pyspark = ["pyspark (>=2.4.4)"]
|
||||
quart = ["blinker (>=1.1)", "quart (>=0.16.1)"]
|
||||
@ -1030,19 +1038,19 @@ aiomysql = ["aiomysql", "greenlet (!=0.4.17)"]
|
||||
aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"]
|
||||
asyncio = ["greenlet (!=0.4.17)"]
|
||||
asyncmy = ["asyncmy (>=0.2.3,!=0.2.4)", "greenlet (!=0.4.17)"]
|
||||
mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2)"]
|
||||
mariadb_connector = ["mariadb (>=1.0.1,!=1.1.2)"]
|
||||
mssql = ["pyodbc"]
|
||||
mssql-pymssql = ["pymssql"]
|
||||
mssql-pyodbc = ["pyodbc"]
|
||||
mssql_pymssql = ["pymssql"]
|
||||
mssql_pyodbc = ["pyodbc"]
|
||||
mypy = ["mypy (>=0.910)", "sqlalchemy2-stubs"]
|
||||
mysql = ["mysqlclient (>=1.4.0)", "mysqlclient (>=1.4.0,<2)"]
|
||||
mysql-connector = ["mysql-connector-python"]
|
||||
mysql_connector = ["mysql-connector-python"]
|
||||
oracle = ["cx_oracle (>=7)", "cx_oracle (>=7,<8)"]
|
||||
postgresql = ["psycopg2 (>=2.7)"]
|
||||
postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"]
|
||||
postgresql-pg8000 = ["pg8000 (>=1.16.6,!=1.29.0)"]
|
||||
postgresql-psycopg2binary = ["psycopg2-binary"]
|
||||
postgresql-psycopg2cffi = ["psycopg2cffi"]
|
||||
postgresql_asyncpg = ["asyncpg", "greenlet (!=0.4.17)"]
|
||||
postgresql_pg8000 = ["pg8000 (>=1.16.6,!=1.29.0)"]
|
||||
postgresql_psycopg2binary = ["psycopg2-binary"]
|
||||
postgresql_psycopg2cffi = ["psycopg2cffi"]
|
||||
pymysql = ["pymysql", "pymysql (<1)"]
|
||||
sqlcipher = ["sqlcipher3_binary"]
|
||||
|
||||
@ -1093,6 +1101,17 @@ category = "main"
|
||||
optional = true
|
||||
python-versions = "~=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "thefuzz"
|
||||
version = "0.19.0"
|
||||
description = "Fuzzy string matching in python"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.extras]
|
||||
speedup = ["python-levenshtein (>=0.12)"]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.0.1"
|
||||
@ -1274,7 +1293,7 @@ test = ["pytest", "pytest-asyncio", "flaky"]
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.8"
|
||||
content-hash = "0c5cb6b738281012aa5ade35a22efc33159c38c4ddf58140c48a148aa70cb912"
|
||||
content-hash = "dda8009b7611580764e09bafcb38c2e32804975869364e017fbea69e38524d73"
|
||||
|
||||
[metadata.files]
|
||||
aiofiles = [
|
||||
@ -1398,6 +1417,10 @@ arko-wrapper = [
|
||||
{file = "arko-wrapper-0.2.4.tar.gz", hash = "sha256:92b7771c72f3b18d5193b8b4375f1b0f52cb94d3933b00118c281d6e582819ae"},
|
||||
{file = "arko_wrapper-0.2.4-py3-none-any.whl", hash = "sha256:703dcfeeb95c43631d8cba02eebbc57973a3f0bdca7353dfc57b38b257a94774"},
|
||||
]
|
||||
async-lru = [
|
||||
{file = "async-lru-1.0.3.tar.gz", hash = "sha256:c2cb9b2915eb14e6cf3e717154b40f715bf90e596d73623677affd0d1fbcd32a"},
|
||||
{file = "async_lru-1.0.3-py3-none-any.whl", hash = "sha256:ea692c303feb6211ff260d230dae1583636f13e05c9ae616eada77855b7f415c"},
|
||||
]
|
||||
async-timeout = [
|
||||
{file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"},
|
||||
{file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"},
|
||||
@ -2280,6 +2303,10 @@ tgcrypto = [
|
||||
{file = "TgCrypto-1.2.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d6b0c2dc84e632ce7b3d0b767cfe20967e557ad7d71ea5dbd7df2dd544323181"},
|
||||
{file = "TgCrypto-1.2.5.tar.gz", hash = "sha256:9bc2cac6fb9a12ef5b08f3dd500174fe374d89b660cce981f57e3138559cb682"},
|
||||
]
|
||||
thefuzz = [
|
||||
{file = "thefuzz-0.19.0-py2.py3-none-any.whl", hash = "sha256:4fcdde8e40f5ca5e8106bc7665181f9598a9c8b18b0a4d38c41a095ba6788972"},
|
||||
{file = "thefuzz-0.19.0.tar.gz", hash = "sha256:6f7126db2f2c8a54212b05e3a740e45f4291c497d75d20751728f635bb74aa3d"},
|
||||
]
|
||||
tomli = [
|
||||
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
|
||||
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
||||
|
@ -39,6 +39,8 @@ uvicorn = {extras = ["standard"], version = "^0.19.0"}
|
||||
sentry-sdk = "^1.11.0"
|
||||
GitPython = "^3.1.29"
|
||||
openpyxl = "^3.0.10"
|
||||
async-lru = "^1.0.3"
|
||||
thefuzz = "^0.19.0"
|
||||
|
||||
[tool.poetry.extras]
|
||||
pyro = ["Pyrogram", "TgCrypto"]
|
||||
|
Loading…
Reference in New Issue
Block a user