PK!CQ>botx/__init__.pyfrom .bot import AsyncBot, CommandHandler from .bot import SyncBot as Bot from .core import BotXException from .types import ( BubbleElement, ChatTypeEnum, CommandUIElement, File, KeyboardElement, MenuCommand, Message, MessageCommand, MessageUser, RequestTypeEnum, ResponseCommand, ResponseCommandResult, ResponseFile, ResponseNotification, ResponseNotificationResult, ResponseRecipientsEnum, ResponseResult, Status, StatusEnum, StatusResult, SyncID, ) PK!5?ccbotx/bot/__init__.pyfrom .asyncbot import AsyncBot from .dispatcher import CommandHandler from .syncbot import SyncBot PK!j|botx/bot/asyncbot.pyimport aiohttp from typing import Any, BinaryIO, Dict, List, NoReturn, Optional, TextIO, Union from uuid import UUID from botx.types import ( BubbleElement, KeyboardElement, ResponseCommand, ResponseCommandResult, ResponseFile, ResponseNotification, ResponseNotificationResult, ResponseRecipientsEnum, Status, SyncID, ) from .basebot import BaseBot from .dispatcher.asyncdispatcher import AsyncDispatcher class AsyncBot(BaseBot): bot_id: UUID bot_host: str _session: aiohttp.ClientSession def __init__(self): super().__init__() self._dispatcher = AsyncDispatcher() self._session = aiohttp.ClientSession() async def start(self) -> NoReturn: await self._dispatcher.start() async def stop(self) -> NoReturn: await self._dispatcher.shutdown() async def parse_status(self) -> Status: return await self._dispatcher.parse_request({}, request_type="status") async def parse_command(self, data: Dict[str, Any]) -> bool: return await self._dispatcher.parse_request(data, request_type="command") async def send_message( self, text: str, chat_id: Union[SyncID, UUID, List[UUID]], bot_id: UUID, host: str, *, recipients: Union[List[UUID], str] = ResponseRecipientsEnum.all, bubble: Optional[List[List[BubbleElement]]] = None, keyboard: Optional[List[List[KeyboardElement]]] = None, ) -> str: if not bubble: bubble = [] if not keyboard: keyboard = [] if isinstance(chat_id, SyncID): return await self._send_command_result( text=text, chat_id=chat_id, bot_id=bot_id, host=host, recipients=recipients, bubble=bubble, keyboard=keyboard, ) elif isinstance(chat_id, UUID) or isinstance(chat_id, list): group_chat_ids = [] if isinstance(chat_id, UUID): group_chat_ids.append(chat_id) elif isinstance(chat_id, list): group_chat_ids = chat_id print("here") return await self._send_notification_result( text=text, group_chat_ids=group_chat_ids, bot_id=bot_id, host=host, recipients=recipients, bubble=bubble, keyboard=keyboard, ) async def _send_command_result( self, text: str, chat_id: SyncID, bot_id: UUID, host: str, recipients: Union[List[UUID], str], bubble: List[List[BubbleElement]], keyboard: List[List[KeyboardElement]], ) -> str: response_result = ResponseCommandResult( body=text, bubble=bubble, keyboard=keyboard ) response = ResponseCommand( bot_id=bot_id, sync_id=str(chat_id), command_result=response_result, recipients=recipients, ).dict() async with self._session.post( self._url_command.format(host), json=response ) as resp: return await resp.text() async def _send_notification_result( self, text: str, group_chat_ids: List[UUID], bot_id: UUID, host: str, recipients: Union[List[UUID], str], bubble: List[List[BubbleElement]], keyboard: List[List[KeyboardElement]], ) -> str: response_result = ResponseNotificationResult( body=text, bubble=bubble, keyboard=keyboard ) response = ResponseNotification( bot_id=bot_id, notification=response_result, group_chat_ids=group_chat_ids, recipients=recipients, ).dict() async with self._session.post( self._url_notification.format(host), json=response ) as resp: return await resp.text() async def send_file( self, file: Union[TextIO, BinaryIO], chat_id: Union[SyncID, UUID], bot_id: UUID, host: str, ) -> str: response = ResponseFile(bot_id=bot_id, sync_id=chat_id, file=file).dict() response["file"] = file async with self._session.post( self._url_file.format(host), data=response ) as resp: return await resp.text() PK!botx/bot/basebot.pyimport abc from functools import partial from typing import ( Any, BinaryIO, Callable, Dict, List, NoReturn, Optional, TextIO, Union, ) from uuid import UUID from botx.types import ( BubbleElement, KeyboardElement, ResponseRecipientsEnum, Status, SyncID, ) from .dispatcher.basedispatcher import BaseDispatcher from .dispatcher.commandhandler import CommandHandler class BaseBot(abc.ABC): _dispatcher: BaseDispatcher _url_command: str = "https://{}/api/v2/botx/command/callback" _url_notification: str = "https://{}/api/v2/botx/notification/callback" _url_file: str = "https://{}/api/v1/botx/file/callback" def add_handler(self, handler: CommandHandler) -> NoReturn: self._dispatcher.add_handler(handler) def command( self, func: Optional[Callable] = None, *, name: Optional[str] = None, description: Optional[str] = None, body: Optional[str] = None, use_as_default_handler: bool = False, exclude_from_status: bool = False, system_command_handler: bool = False, ) -> Optional[Callable]: if func: name = name or "".join( func.__name__.lower().rsplit("command", 1)[0].rsplit("_", 1) ) body = ( (body if body.startswith("/") or system_command_handler else f"/{body}") if body else f"/{name}" ) description = description or f"{name} command" self.add_handler( CommandHandler( command=body, func=func, name=name.capitalize(), description=description, exclude_from_status=exclude_from_status, use_as_default_handler=use_as_default_handler, system_command_handler=system_command_handler, ) ) return func else: return partial( self.command, name=name, description=description, body=body, exclude_from_status=exclude_from_status, use_as_default_handler=use_as_default_handler, system_command_handler=system_command_handler, ) @abc.abstractmethod def start(self) -> NoReturn: # pragma: no cover pass @abc.abstractmethod def parse_status(self) -> Status: # pragma: no cover pass @abc.abstractmethod def parse_command(self, data: Dict[str, Any]) -> bool: # pragma: no cover pass @abc.abstractmethod def send_message( self, text: str, chat_id: Union[SyncID, UUID, List[UUID]], bot_id: UUID, host: str, *, recipients: Union[List[UUID], str] = ResponseRecipientsEnum.all, bubble: Optional[List[List[BubbleElement]]] = None, keyboard: Optional[List[List[KeyboardElement]]] = None, ) -> str: # pragma: no cover pass @abc.abstractmethod def _send_command_result( self, text: str, chat_id: SyncID, bot_id: UUID, host: str, recipients: Union[List[UUID], str], bubble: List[List[BubbleElement]], keyboard: List[List[KeyboardElement]], ) -> str: # pragma: no cover pass @abc.abstractmethod def _send_notification_result( self, text: str, group_chat_ids: List[UUID], bot_id: UUID, host: str, recipients: Union[List[UUID], str], bubble: List[List[BubbleElement]], keyboard: List[List[KeyboardElement]], ) -> str: # pragma: no cover pass @abc.abstractmethod def send_file( self, file: Union[TextIO, BinaryIO], chat_id: Union[SyncID, UUID], bot_id: UUID, host: str, ) -> str: # pragma: no cover pass PK!vJ++botx/bot/dispatcher/__init__.pyfrom .commandhandler import CommandHandler PK!&botx/bot/dispatcher/asyncdispatcher.pyimport aiojobs import inspect from typing import Any, Dict, NoReturn, Union from botx.core import BotXException from botx.types import Message, RequestTypeEnum, Status, SyncID from .basedispatcher import BaseDispatcher from .commandhandler import CommandHandler class AsyncDispatcher(BaseDispatcher): _scheduler = aiojobs.Scheduler def __init__(self): super().__init__() async def start(self) -> NoReturn: self._scheduler = await aiojobs.create_scheduler() async def shutdown(self) -> NoReturn: await self._scheduler.close() async def parse_request( self, data: Dict[str, Any], request_type: Union[str, RequestTypeEnum] ) -> Union[Status, bool]: if request_type == RequestTypeEnum.status: return self._create_status() elif request_type == RequestTypeEnum.command: return await self._create_message(data) async def _create_message(self, data: Dict[str, Any]) -> bool: message = Message(**data) message.sync_id = SyncID(str(message.sync_id)) cmd = message.command.cmd command = self._handlers.get(cmd) if command: await self._scheduler.spawn(command.func(message)) return True else: if self._default_handler: await self._scheduler.spawn(self._default_handler.func(message)) return True return False def add_handler(self, handler: CommandHandler) -> NoReturn: if not inspect.iscoroutinefunction(handler.func): raise BotXException("can not add not async handler to async dispatcher") super().add_handler(handler) PK!=~%botx/bot/dispatcher/basedispatcher.pyfrom collections import OrderedDict import abc from typing import Any, Awaitable, Dict, NoReturn, Optional, Union from botx.types import RequestTypeEnum, Status, StatusResult from .commandhandler import CommandHandler class BaseDispatcher(abc.ABC): _handlers: Dict[str, CommandHandler] _default_handler: Optional[CommandHandler] = None def __init__(self): self._handlers = OrderedDict() @abc.abstractmethod def start(self) -> NoReturn: # pragma: no cover pass @abc.abstractmethod def shutdown(self) -> NoReturn: # pragma: no cover pass @abc.abstractmethod def parse_request( self, data: Dict[str, Any], request_type: Union[str, RequestTypeEnum] ) -> Union[Status, bool]: # pragma: no cover pass @abc.abstractmethod def _create_message( self, data: Dict[str, Any] ) -> Union[Awaitable, bool]: # pragma: no cover pass def _create_status(self) -> Status: commands = [] for command_name, handler in self._handlers.items(): menu_command = handler.to_status_command() if menu_command: commands.append(menu_command) return Status(result=StatusResult(commands=commands)) def add_handler(self, handler: CommandHandler) -> NoReturn: if handler.use_as_default_handler: self._default_handler = handler else: self._handlers[handler.command] = handler PK!(%botx/bot/dispatcher/commandhandler.pyfrom typing import Any, Callable, Dict, List, Optional from botx.core import BotXObject from botx.types import CommandUIElement, MenuCommand class CommandHandler(BotXObject): name: str command: str description: str func: Callable exclude_from_status: bool = False use_as_default_handler: bool = False options: Dict[str, Any] = {} elements: List[CommandUIElement] = [] system_command_handler: bool = False def to_status_command(self) -> Optional[MenuCommand]: if ( not self.exclude_from_status and not self.use_as_default_handler and not self.system_command_handler ): return MenuCommand( body=self.command, name=self.name, description=self.description, options=self.options, elements=self.elements, ) PK!WO}}%botx/bot/dispatcher/syncdispatcher.pyimport inspect import logging import functools from concurrent.futures.thread import ThreadPoolExecutor from typing import Any, Dict, NoReturn, Union from botx.core import BotXException from botx.types import Message, RequestTypeEnum, Status, SyncID from .basedispatcher import BaseDispatcher from .commandhandler import CommandHandler class SyncDispatcher(BaseDispatcher): _pool: ThreadPoolExecutor def __init__(self, workers: int): super().__init__() self._pool = ThreadPoolExecutor(max_workers=workers) def start(self) -> NoReturn: pass def shutdown(self) -> NoReturn: self._pool.shutdown() def parse_request( self, data: Dict[str, Any], request_type: Union[str, RequestTypeEnum] ) -> Union[Status, bool]: if request_type == RequestTypeEnum.status: return self._create_status() elif request_type == RequestTypeEnum.command: return self._create_message(data) else: raise BotXException(f"wrong request type {repr(request_type)}") def _create_message(self, data: Dict[str, Any]) -> bool: message = Message(**data) message.sync_id = SyncID(str(message.sync_id)) cmd = message.command.cmd command = self._handlers.get(cmd) if command: self._pool.submit(command.func, message) return True else: if self._default_handler: self._pool.submit(self._default_handler.func, message=message) return True return False def add_handler(self, handler: CommandHandler) -> NoReturn: if inspect.iscoroutinefunction(handler.func): raise BotXException("can not add async handler to sync dispatcher") def thread_logger_helper(f): @functools.wraps(f) def wrapper(message, bot): try: return f(message, bot) except Exception as e: logging.exception(e) return e return wrapper handler.func = thread_logger_helper(handler.func) super().add_handler(handler) PK!˚.botx/bot/syncbot.pyimport multiprocessing import requests from typing import Any, BinaryIO, Dict, List, NoReturn, Optional, TextIO, Union from uuid import UUID from botx.types import ( BubbleElement, KeyboardElement, ResponseCommand, ResponseCommandResult, ResponseFile, ResponseNotification, ResponseNotificationResult, ResponseRecipientsEnum, Status, SyncID, ) from .basebot import BaseBot from .dispatcher.syncdispatcher import SyncDispatcher class SyncBot(BaseBot): bot_id: UUID bot_host: str _dispatcher: SyncDispatcher _workers: Optional[int] = multiprocessing.cpu_count() def __init__(self): super().__init__() self._dispatcher = SyncDispatcher(workers=self._workers) def start(self) -> NoReturn: self._dispatcher.start() def stop(self) -> NoReturn: self._dispatcher.shutdown() def parse_status(self) -> Status: return self._dispatcher.parse_request({}, request_type="status") def parse_command(self, data: Dict[str, Any]) -> bool: return self._dispatcher.parse_request(data, request_type="command") def send_message( self, text: str, chat_id: Union[SyncID, UUID, List[UUID]], bot_id: UUID, host: str, *, recipients: Union[List[UUID], str] = ResponseRecipientsEnum.all, bubble: Optional[List[List[BubbleElement]]] = None, keyboard: Optional[List[List[KeyboardElement]]] = None, ) -> str: if not bubble: bubble = [] if not keyboard: keyboard = [] if isinstance(chat_id, SyncID): return self._send_command_result( text=text, chat_id=chat_id, bot_id=bot_id, host=host, recipients=recipients, bubble=bubble, keyboard=keyboard, ) elif isinstance(chat_id, UUID) or isinstance(chat_id, list): group_chat_ids = [] if isinstance(chat_id, UUID): group_chat_ids.append(chat_id) elif isinstance(chat_id, list): group_chat_ids = chat_id return self._send_notification_result( text=text, group_chat_ids=group_chat_ids, bot_id=bot_id, host=host, recipients=recipients, bubble=bubble, keyboard=keyboard, ) def _send_command_result( self, text: str, chat_id: SyncID, bot_id: UUID, host: str, recipients: Union[List[UUID], str], bubble: List[List[BubbleElement]], keyboard: List[List[KeyboardElement]], ) -> str: response_result = ResponseCommandResult( body=text, bubble=bubble, keyboard=keyboard ) response = ResponseCommand( bot_id=bot_id, sync_id=str(chat_id), command_result=response_result, recipients=recipients, ) resp = requests.post(self._url_command.format(host), json=response.dict()) return resp.text def _send_notification_result( self, text: str, group_chat_ids: List[UUID], bot_id: UUID, host: str, recipients: Union[List[UUID], str], bubble: List[List[BubbleElement]], keyboard: List[List[KeyboardElement]], ) -> str: response_result = ResponseNotificationResult( body=text, bubble=bubble, keyboard=keyboard ) response = ResponseNotification( bot_id=bot_id, notification=response_result, group_chat_ids=group_chat_ids, recipients=recipients, ) resp = requests.post(self._url_notification.format(host), json=response.dict()) return resp.text def send_file( self, file: Union[TextIO, BinaryIO], chat_id: Union[SyncID, UUID], bot_id: UUID, host: str, ) -> str: files = {"file": file} response = ResponseFile(bot_id=bot_id, sync_id=chat_id).dict() return requests.post( self._url_file.format(host), files=files, data=response ).text PK!T krr botx/core.pyfrom pydantic import BaseModel class BotXObject(BaseModel): pass class BotXException(Exception): pass PK! botx/types/__init__.pyfrom .bubble import BubbleElement from .command import MessageCommand from .core import ( ChatTypeEnum, CommandUIElement, MenuCommand, RequestTypeEnum, ResponseRecipientsEnum, StatusEnum, SyncID, ) from .file import File from .keyboard import KeyboardElement from .message import Message, MessageUser from .response import ( ResponseCommand, ResponseCommandResult, ResponseFile, ResponseNotification, ResponseNotificationResult, ResponseResult, ) from .status import Status, StatusResult PK!Չbotx/types/base.pyimport json from uuid import UUID from pydantic import BaseConfig from botx.core import BotXObject class _UUIDJSONEncoder(json.JSONEncoder): def default(self, o): if isinstance(o, UUID): return str(o) return super().default(o) class BotXType(BotXObject): class Config(BaseConfig): allow_population_by_alias = True def json(self, *, by_alias: bool = True, **kwargs): return super().json(by_alias=by_alias, **kwargs) def dict(self, *, by_alias: bool = True, **kwargs): return json.loads( json.dumps(super().dict(by_alias=by_alias, **kwargs), cls=_UUIDJSONEncoder) ) PK!J(botx/types/bubble.pyfrom typing import Optional from .base import BotXType class BubbleElement(BotXType): command: str label: Optional[str] = None def __init__(self, **data): super().__init__(**data) self.label = self.label or self.command PK!W!;;botx/types/command.pyfrom typing import Any, Dict from .base import BotXType class MessageCommand(BotXType): body: str data: Dict[str, Any] = {} @property def cmd(self) -> str: return self.body.split(" ", 1)[0] @property def cmd_arg(self) -> str: return "".join(self.body.split(" ", 1)[1:]) PK!8botx/types/core.pyfrom enum import Enum from typing import Any, Dict, List, Optional from uuid import UUID from .base import BotXType class StatusEnum(str, Enum): ok: str = "ok" error: str = "error" class ResponseRecipientsEnum(str, Enum): all: str = "all" class ChatTypeEnum(str, Enum): chat: str = "chat" group_chat: str = "group_chat" class RequestTypeEnum(str, Enum): status: str = "status" command: str = "command" class CommandUIElement(BotXType): type: str label: str order: Optional[int] = None value: Optional[Any] = None disabled: Optional[bool] = None class MenuCommand(BotXType): description: str body: str name: str options: Dict[str, Any] = {} elements: List[CommandUIElement] = [] class SyncID(UUID): pass PK!?botx/types/file.pyimport base64 from io import BytesIO from typing import BinaryIO, Union from .base import BotXType class File(BotXType): data: str file_name: str @property def file(self) -> Union[BinaryIO]: d = BytesIO(self.raw_data) d.name = self.file_name return d @property def raw_data(self) -> bytes: return base64.b64decode(self.data.split(",", 1)[1]) PK!0botx/types/keyboard.pyfrom typing import Optional from .base import BotXType class KeyboardElement(BotXType): command: str label: Optional[str] = None def __init__(self, **data): super().__init__(**data) self.label = self.label or self.command PK!hbotx/types/message.pyfrom typing import Any, Dict, Optional from uuid import UUID from pydantic import Schema from .base import BotXType from .command import MessageCommand from .core import ChatTypeEnum, SyncID from .file import File class MessageUser(BotXType): user_huid: Optional[UUID] group_chat_id: UUID chat_type: ChatTypeEnum ad_login: Optional[str] ad_domain: Optional[str] username: Optional[str] host: str class Message(BotXType): sync_id: SyncID command: MessageCommand file: Optional[File] = None user: MessageUser = Schema(..., alias="from") bot_id: UUID @property def body(self) -> str: return self.command.body @property def data(self) -> Dict[str, Any]: return self.command.data @property def user_huid(self) -> Optional[UUID]: return self.user.user_huid @property def ad_login(self) -> Optional[str]: return self.user.ad_login @property def group_chat_id(self) -> UUID: return self.user.group_chat_id @property def chat_type(self) -> ChatTypeEnum: return self.user.chat_type @property def host(self) -> str: return self.user.host PK!Djjbotx/types/response.pyfrom typing import Any, List, Optional, Union from uuid import UUID from .base import BotXType from .bubble import BubbleElement from .core import MenuCommand, ResponseRecipientsEnum, StatusEnum, SyncID from .keyboard import KeyboardElement class ResponseResult(BotXType): status: StatusEnum = StatusEnum.ok body: str commands: List[MenuCommand] = [] keyboard: List[List[KeyboardElement]] = [] bubble: List[List[BubbleElement]] = [] class ResponseCommandResult(ResponseResult): pass class ResponseNotificationResult(ResponseResult): pass class ResponseCommand(BotXType): sync_id: SyncID bot_id: UUID recipients: Union[List[UUID], ResponseRecipientsEnum] = ResponseRecipientsEnum.all command_result: ResponseCommandResult file: Optional[str] = None class ResponseNotification(BotXType): bot_id: UUID recipients: Union[List[UUID], ResponseRecipientsEnum] = ResponseRecipientsEnum.all group_chat_ids: List[UUID] = [] notification: ResponseNotificationResult file: Optional[str] = None class ResponseFile(BotXType): bot_id: UUID sync_id: SyncID PK!G vRRbotx/types/status.pyfrom typing import List from .base import BotXType from .core import MenuCommand, StatusEnum class StatusResult(BotXType): enabled: bool = True status_message: str = "Bot is working" commands: List[MenuCommand] = [] class Status(BotXType): status: StatusEnum = StatusEnum.ok result: StatusResult = StatusResult() PK!166botx-0.9.1.dist-info/LICENSEMIT License Copyright (c) 2019 Unlimited Technologies Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.PK!HڽTUbotx-0.9.1.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]\fi4WZ^EgM_-]#0(q7PK!Hp`botx-0.9.1.dist-info/METADATA]O0+Υ&OAP6p[uh;z;n;yyzThbQ04Q VBsA3+8'arбȀe4f@ZZ|c,(4.ᜡZQ BQ9ܾ4vB=:˶\)Ĥ,v11Zf;j⣞]-*a[p\fgCջLG zTUP*^mZfnc zGƊ  k0P ԖrʼMmTC,0z-,J}ڃxlfOI%Dl"l!G]m@@F 9jz`R՝Xx(p= 󌐢Q?O8< J5mEVD '8|Ū%}}ivq'@˦c*6m\G \}0,_-~rΰ2'=# 6)7<[*B@ZZѺc/6JroWB#YeKGE{;=sۏ9Ï=*u l~$ [R[}\ybJO@sQ؜i*gB"]H#W[2(HW+REEt#p۶nBZ _W\o0_Oi5`2e" C_*+6ej[;ɘ)9"iR,,iɔ.AZ( c0N X@[--M:zq\j{58ӂwsG8Y6=_#( yt)إl~`5\7MoE=EPA]}0jF^6|y0(Q ژϠ쁵ǝ%JEE-qT`F2botx/__init__.pyPK!5?ccGbotx/bot/__init__.pyPK!j|botx/bot/asyncbot.pyPK!botx/bot/basebot.pyPK!vJ++$botx/bot/dispatcher/__init__.pyPK!&$botx/bot/dispatcher/asyncdispatcher.pyPK!=~%+botx/bot/dispatcher/basedispatcher.pyPK!(%1botx/bot/dispatcher/commandhandler.pyPK!WO}}%5botx/bot/dispatcher/syncdispatcher.pyPK!˚.G>botx/bot/syncbot.pyPK!T krr 6Obotx/core.pyPK! Obotx/types/__init__.pyPK!Չ"Rbotx/types/base.pyPK!J(Tbotx/types/bubble.pyPK!W!;;Vbotx/types/command.pyPK!8Wbotx/types/core.pyPK!?Zbotx/types/file.pyPK!0\botx/types/keyboard.pyPK!h]botx/types/message.pyPK!Djjbbotx/types/response.pyPK!G vRR?gbotx/types/status.pyPK!166hbotx-0.9.1.dist-info/LICENSEPK!HڽTU3mbotx-0.9.1.dist-info/WHEELPK!Hp`mbotx-0.9.1.dist-info/METADATAPK!HٯDdZobotx-0.9.1.dist-info/RECORDPKs