├── src └── koishi │ ├── adapter.py │ ├── __init__.py │ ├── command.py │ ├── element.py │ ├── models.py │ ├── session.py │ ├── context.py │ └── bot.py ├── README.md ├── LICENSE ├── main.py ├── pyproject.toml ├── .gitignore └── pdm.lock /src/koishi/adapter.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, Generic, TypeVar 2 | 3 | from .bot import Bot 4 | 5 | if TYPE_CHECKING: 6 | from .context import Context 7 | 8 | B = TypeVar("B", bound=Bot) 9 | 10 | 11 | class Adapter(Generic[B]): 12 | schema = False 13 | 14 | bots: list[B] 15 | 16 | def __init__(self, ctx: "Context"): 17 | self.context = ctx 18 | self.bots = [] 19 | 20 | def connect(self, bot: B): 21 | self.bots.append(bot) 22 | 23 | def disconnect(self, bot: B): 24 | self.bots.remove(bot) 25 | 26 | def fork(self, ctx: "Context", bot: B): 27 | bot.adapter = self 28 | self.bots.append(bot) 29 | ctx.on("dispose", lambda: self.disconnect(bot)) 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # koishi-python 2 | 3 | 在 python 下运行 koishi 的实例 4 | 5 | ## 安装 6 | 7 | ```bash 8 | pip install "koishi-python @ git+https://github.com/koishipy/koishi-python" 9 | ``` 10 | 11 | ## 使用 12 | 13 | ```python 14 | from koishi import Context 15 | 16 | ctx = Context({}) 17 | 18 | ctx.requires( 19 | "@koishijs/plugin-console", 20 | "@koishijs/plugin-sandbox", 21 | "@koishijs/plugin-server", 22 | config={ 23 | "@koishijs/plugin-server": {"port": 5140}, 24 | }, 25 | ) 26 | 27 | ctx.command("echo ", "输出收到的信息", {"checkArgCount": True})\ 28 | .action(lambda _, __, *args: args[0]) 29 | 30 | ctx.run() 31 | ``` 32 | 33 | ## 注意事项 34 | 35 | 1. 你 require 的 npm 包会自动安装,但是每安装完一次会报错然后退出,这是正常现象,不用担心。 36 | 2. 启动后会出现一些警告信息,这些对运行没有影响 37 | 3. 目前默认安装 koishi 最新版 38 | 4. 因为运行位置在 site-packages 下,`koishi` 会自动在运行目录下建立软链接。 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Koishi.py 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/koishi/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | 4 | import javascript 5 | 6 | _koishi = javascript.require("koishi") 7 | _Context = _koishi["Context"] 8 | main_path = Path.cwd() 9 | js_path = Path(javascript.__file__) 10 | node_src = js_path.parent / "js" / "node_modules" 11 | node_src.mkdir(parents=True, exist_ok=True) 12 | node_dst = main_path / "node_modules" 13 | if node_src.exists() and not node_dst.exists(): 14 | os.symlink(node_src.as_posix(), node_dst.as_posix(), target_is_directory=True) 15 | package_src = js_path.parent / "js" / "package.json" 16 | package_lock_src = js_path.parent / "js" / "package-lock.json" 17 | package_dst = main_path / "package.json" 18 | package_lock_dst = main_path / "package-lock.json" 19 | if package_src.exists() and not package_dst.exists(): 20 | os.symlink(package_src.as_posix(), package_dst.as_posix()) 21 | if package_lock_src.exists() and not package_lock_dst.exists(): 22 | os.symlink(package_lock_src.as_posix(), package_lock_dst.as_posix()) 23 | 24 | from .context import Context as Context 25 | from .element import Element as Element 26 | from .element import h as h 27 | from .session import Session as Session 28 | 29 | Context.__context_type__ = _Context 30 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from koishi import Context, Session, h 2 | 3 | ctx = Context({}) 4 | 5 | ctx.requires( 6 | "@koishijs/plugin-console", 7 | "@koishijs/plugin-sandbox", 8 | "@koishijs/plugin-echo", 9 | "@koishijs/plugin-help", 10 | "@koishijs/plugin-server", 11 | config={ 12 | "@koishijs/plugin-server": {"port": 5140}, 13 | }, 14 | ) 15 | 16 | 17 | def test(x: Context, *_): 18 | def handle(session: Session, *args): 19 | if session.content == "天王盖地虎": 20 | session.send(["宝塔镇河妖", h.at(session.event.user.id, {"name": session.event.user.name})]) 21 | bot = x.bots[session.sid] 22 | session.send(repr(bot.getChannel(session.event.channel.id))) 23 | 24 | x.on("message", handle) 25 | 26 | 27 | ctx.plugin({"apply": test}) 28 | ctx.command("echo1 ", "输出收到的信息", {"checkArgCount": True}).option( 29 | "timeout", "-t 设定延迟发送的时间" 30 | ).usage("注意:参数请写在最前面,不然会被当成 message 的一部分!").example( 31 | "echo -t 300 Hello World 五分钟后发送 Hello World" 32 | ).action( 33 | lambda cmd, _, *args: args[0] 34 | ) 35 | 36 | ctx.command("test").option("alpha", "-a").option("beta", "-b [beta]").option("gamma", "-c ").action( 37 | lambda cmd, argv, *args: str(argv.get("options")) 38 | ) 39 | 40 | 41 | ctx.run() 42 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "koishi-python" 3 | version = "0.1.0" 4 | description = "Run Koishi in python" 5 | authors = [ 6 | {name = "rf_tar_railt", email = "rf_tar_railt@qq.com"}, 7 | ] 8 | dependencies = [ 9 | "javascript @ git+https://github.com/koishipy/JSPyBridge@ref_val", 10 | "typing-extensions>=4.7.0", 11 | ] 12 | requires-python = ">=3.9" 13 | readme = "README.md" 14 | license = {text = "MIT"} 15 | 16 | [build-system] 17 | requires = ["pdm-backend"] 18 | build-backend = "pdm.backend" 19 | 20 | 21 | [tool.pdm] 22 | distribution = true 23 | 24 | [tool.pdm.build] 25 | includes = ["src/koishi"] 26 | 27 | [tool.pdm.dev-dependencies] 28 | dev = [ 29 | "isort>=5.13.2", 30 | "black>=24.4.0", 31 | "ruff>=0.3.7", 32 | ] 33 | 34 | 35 | [tool.black] 36 | line-length = 110 37 | include = '\.pyi?$' 38 | extend-exclude = ''' 39 | ''' 40 | 41 | [tool.isort] 42 | profile = "black" 43 | line_length = 110 44 | skip_gitignore = true 45 | extra_standard_library = ["typing_extensions"] 46 | 47 | [tool.ruff] 48 | line-length = 110 49 | target-version = "py38" 50 | exclude = ["exam.py"] 51 | 52 | [tool.ruff.lint] 53 | select = ["E", "W", "F", "UP", "C", "T", "Q"] 54 | ignore = ["E402", "F403", "F405", "C901", "UP037"] 55 | 56 | [tool.pyright] 57 | pythonPlatform = "All" 58 | pythonVersion = "3.9" 59 | typeCheckingMode = "standard" 60 | reportShadowedImports = false 61 | disableBytesTypePromotions = true 62 | -------------------------------------------------------------------------------- /src/koishi/command.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Awaitable, Callable, Optional, TypedDict, Union, overload 2 | from typing_extensions import Unpack 3 | 4 | from .session import Session 5 | 6 | 7 | class CommandBaseConfig(TypedDict, total=False): 8 | strictOptions: bool 9 | 10 | 11 | class CommandConfig(CommandBaseConfig, total=False): 12 | checkUnknown: bool 13 | checkArgCount: bool 14 | showWarning: bool 15 | handleError: Union[bool, Callable[[Exception, Any], Awaitable[Any]]] 16 | slash: bool 17 | 18 | hidden: Union[bool, Callable[..., bool]] 19 | hideOptions: bool 20 | params: dict[str, Any] 21 | 22 | 23 | class PermissionConfig(TypedDict, total=False): 24 | authority: float 25 | permissions: list[str] 26 | dependencies: list[str] 27 | 28 | 29 | class OptionConfig(PermissionConfig, total=False): 30 | aliases: list[str] 31 | symbols: list[str] 32 | fallback: Any 33 | value: Any 34 | type: Any 35 | descPath: str 36 | 37 | hidden: Union[bool, Callable[..., bool]] 38 | params: dict[str, Any] 39 | 40 | 41 | class AliasConfig(TypedDict, total=False): 42 | options: dict[str, Any] 43 | args: list[str] 44 | filter: Union[bool, Callable[..., bool]] 45 | 46 | 47 | class Argv(TypedDict, total=False): 48 | args: list 49 | options: dict[str, Any] 50 | error: str 51 | source: str 52 | initiator: str 53 | terminator: str 54 | session: Session 55 | command: "Command" 56 | rest: str 57 | pos: int 58 | root: bool 59 | tokens: list 60 | name: str 61 | next: Callable[..., Awaitable[Any]] 62 | 63 | 64 | class Command: 65 | def option(self, name: str, desc: str, config: Optional[OptionConfig] = None, /) -> "Command": ... 66 | def action( 67 | self, callback: Callable[["Command", Argv, Unpack[tuple]], Any], prepend: bool = False 68 | ) -> "Command": ... 69 | @overload 70 | def alias(self, *names: str) -> "Command": ... 71 | @overload 72 | def alias(self, name: str, config: Optional[AliasConfig] = None, /) -> "Command": ... 73 | def alias(self, *args) -> "Command": ... 74 | def usage(self, text: str) -> "Command": ... 75 | def example(self, text: str) -> "Command": ... 76 | 77 | @overload 78 | def subcommand(self, name: str, config: Optional[CommandConfig] = None, /) -> "Command": ... 79 | @overload 80 | def subcommand(self, name: str, desc: str, config: Optional[CommandConfig] = None, /) -> "Command": ... 81 | def subcommand(self, *args) -> "Command": ... 82 | -------------------------------------------------------------------------------- /src/koishi/element.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Final, Optional, Union, overload 2 | from typing_extensions import TypeAlias 3 | 4 | import javascript 5 | 6 | _Element = javascript.require("@satorijs/core").Element 7 | 8 | 9 | class Element: 10 | type: str 11 | attrs: dict[str, Any] 12 | children: list["Element"] = [] 13 | 14 | @staticmethod 15 | def text(content: str, attrs: Optional[dict[str, Any]] = None, /) -> "Element": 16 | return _Element["text"](content, attrs) 17 | 18 | @staticmethod 19 | def at(uid: str, attrs: Optional[dict[str, Any]] = None, /) -> "Element": 20 | return _Element["at"](uid, attrs) 21 | 22 | @staticmethod 23 | def sharp(cid: str, attrs: Optional[dict[str, Any]] = None, /) -> "Element": 24 | return _Element["sharp"](cid, attrs) 25 | 26 | @staticmethod 27 | def quote(mid: str, attrs: Optional[dict[str, Any]] = None, /) -> "Element": 28 | return _Element["quote"](mid, attrs) 29 | 30 | @staticmethod 31 | @overload 32 | def image(data: str, attrs: Optional[dict[str, Any]] = None, /) -> "Element": ... 33 | 34 | @staticmethod 35 | @overload 36 | def image(data: bytes, dtype: str, attrs: Optional[dict[str, Any]] = None, /) -> "Element": ... 37 | 38 | @staticmethod 39 | def image(*args) -> "Element": 40 | return _Element["image"](*args) 41 | 42 | @staticmethod 43 | @overload 44 | def img(data: str, attrs: Optional[dict[str, Any]] = None, /) -> "Element": ... 45 | 46 | @staticmethod 47 | @overload 48 | def img(data: bytes, dtype: str, attrs: Optional[dict[str, Any]] = None, /) -> "Element": ... 49 | 50 | @staticmethod 51 | def img(*args) -> "Element": 52 | return _Element["img"](*args) 53 | 54 | @staticmethod 55 | @overload 56 | def video(data: str, attrs: Optional[dict[str, Any]] = None, /) -> "Element": ... 57 | 58 | @staticmethod 59 | @overload 60 | def video(data: bytes, dtype: str, attrs: Optional[dict[str, Any]] = None, /) -> "Element": ... 61 | 62 | @staticmethod 63 | def video(*args) -> "Element": 64 | return _Element["video"](*args) 65 | 66 | @staticmethod 67 | @overload 68 | def audio(data: str, attrs: Optional[dict[str, Any]] = None, /) -> "Element": ... 69 | 70 | @staticmethod 71 | @overload 72 | def audio(data: bytes, dtype: str, attrs: Optional[dict[str, Any]] = None, /) -> "Element": ... 73 | 74 | @staticmethod 75 | def audio(*args) -> "Element": 76 | return _Element["audio"](*args) 77 | 78 | @staticmethod 79 | @overload 80 | def file(data: str, attrs: Optional[dict[str, Any]] = None, /) -> "Element": ... 81 | 82 | @staticmethod 83 | @overload 84 | def file(data: bytes, dtype: str, attrs: Optional[dict[str, Any]] = None, /) -> "Element": ... 85 | 86 | @staticmethod 87 | def file(*args) -> "Element": 88 | return _Element["file"](*args) 89 | 90 | @staticmethod 91 | def i18n(path: Union[str, dict[str, Any]], children: Optional[list] = None, /) -> "Element": 92 | return _Element["i18n"](path, children) 93 | 94 | 95 | h: Final = Element 96 | Fragment: TypeAlias = Union[str, Element, list[Union[str, Element]]] 97 | -------------------------------------------------------------------------------- /src/koishi/models.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from datetime import datetime 3 | from enum import IntEnum 4 | from typing import Any, Generic, List, Optional, TypeVar 5 | 6 | 7 | class ChannelType(IntEnum): 8 | TEXT = 0 9 | DIRECT = 1 10 | CATEGORY = 2 11 | VOICE = 3 12 | 13 | 14 | @dataclass 15 | class Channel: 16 | id: str 17 | type: ChannelType 18 | name: Optional[str] = None 19 | parentId: Optional[str] = None 20 | 21 | 22 | @dataclass 23 | class Guild: 24 | id: str 25 | name: Optional[str] = None 26 | avatar: Optional[str] = None 27 | 28 | 29 | @dataclass 30 | class User: 31 | id: str 32 | name: Optional[str] = None 33 | nick: Optional[str] = None 34 | avatar: Optional[str] = None 35 | isBot: Optional[bool] = None 36 | 37 | 38 | @dataclass 39 | class Member: 40 | user: Optional[User] = None 41 | nick: Optional[str] = None 42 | name: Optional[str] = None 43 | avatar: Optional[str] = None 44 | joinedAt: Optional[int] = None 45 | 46 | 47 | @dataclass 48 | class Role: 49 | id: str 50 | name: Optional[str] = None 51 | 52 | 53 | class LoginStatus(IntEnum): 54 | OFFLINE = 0 55 | ONLINE = 1 56 | CONNECT = 2 57 | DISCONNECT = 3 58 | RECONNECT = 4 59 | 60 | 61 | @dataclass 62 | class Login: 63 | status: LoginStatus 64 | user: Optional[User] = None 65 | selfId: Optional[str] = None 66 | platform: Optional[str] = None 67 | hidden: Optional[bool] = None 68 | 69 | 70 | @dataclass 71 | class ArgvInteraction: 72 | name: str 73 | arguments: list 74 | options: Any 75 | 76 | 77 | @dataclass 78 | class ButtonInteraction: 79 | id: str 80 | 81 | 82 | class Opcode(IntEnum): 83 | EVENT = 0 84 | PING = 1 85 | PONG = 2 86 | IDENTIFY = 3 87 | READY = 4 88 | 89 | 90 | @dataclass 91 | class Identify: 92 | token: Optional[str] = None 93 | sequence: Optional[int] = None 94 | 95 | 96 | @dataclass 97 | class Ready: 98 | logins: List[Login] 99 | 100 | 101 | @dataclass 102 | class MessageObject: 103 | id: str 104 | content: str 105 | channel: Optional[Channel] = None 106 | guild: Optional[Guild] = None 107 | member: Optional[Member] = None 108 | user: Optional[User] = None 109 | createdAt: Optional[int] = None 110 | updatedAt: Optional[int] = None 111 | 112 | 113 | @dataclass 114 | class Event: 115 | id: int 116 | type: str 117 | platform: str 118 | selfId: str 119 | timestamp: datetime 120 | argv: Optional[ArgvInteraction] = None 121 | button: Optional[ButtonInteraction] = None 122 | channel: Optional[Channel] = None 123 | guild: Optional[Guild] = None 124 | login: Optional[Login] = None 125 | member: Optional[Member] = None 126 | message: Optional[MessageObject] = None 127 | operator: Optional[User] = None 128 | role: Optional[Role] = None 129 | user: Optional[User] = None 130 | 131 | _type: Optional[str] = None 132 | _data: Optional[dict] = None 133 | 134 | 135 | T = TypeVar("T") 136 | 137 | 138 | @dataclass 139 | class PageResult(Generic[T]): 140 | data: List[T] 141 | next: Optional[str] = None 142 | -------------------------------------------------------------------------------- /src/koishi/session.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable 2 | from typing import TYPE_CHECKING, Any, Iterable, List, Literal, Optional, TypedDict, TypeVar, Union 3 | from typing_extensions import overload 4 | 5 | from .element import Fragment 6 | from .models import Channel, Event, Guild, Member, User 7 | 8 | if TYPE_CHECKING: 9 | from .context import Context 10 | 11 | T = TypeVar("T") 12 | 13 | 14 | class Stripped(TypedDict): 15 | content: str 16 | prefix: str 17 | appel: bool 18 | hasAt: bool 19 | atSelf: bool 20 | 21 | 22 | class SendOptions(TypedDict, total=False): 23 | session: "Session" 24 | linkPreview: bool 25 | 26 | 27 | class PromptOptions(TypedDict, total=False): 28 | timeout: float 29 | 30 | 31 | class SuggestOptions(TypedDict, total=False): 32 | actual: bool 33 | expect: list[str] 34 | filter: Callable[[str], bool] 35 | prefix: str 36 | suffix: str 37 | timeout: float 38 | 39 | 40 | class Session: 41 | id: int 42 | app: "Context" 43 | event: Event 44 | locales: List[str] = [] 45 | 46 | user: Optional[User] = None 47 | channel: Optional[Channel] = None 48 | guild: Optional[Guild] = None 49 | permissions: List[str] = [] 50 | scope: Optional[str] = None 51 | response: Optional[Any] = None 52 | 53 | _stripped: Stripped 54 | 55 | @property 56 | def isDirect(self) -> bool: 57 | return self.channel and self.channel.type == "private" # type: ignore 58 | 59 | @property 60 | def author(self) -> Union[User, Member]: 61 | return self.user if self.isDirect else self.event.author # type: ignore 62 | 63 | @property 64 | def uid(self) -> str: 65 | return self.user.id # type: ignore 66 | 67 | @property 68 | def gid(self) -> str: 69 | return self.guild.id # type: ignore 70 | 71 | @property 72 | def cid(self) -> str: 73 | return self.channel.id # type: ignore 74 | 75 | @property 76 | def fid(self) -> str: 77 | return self.event.message.id # type: ignore 78 | 79 | @property 80 | def sid(self) -> str: 81 | return self.event.self_id # type: ignore 82 | 83 | @property 84 | def elements(self) -> List[Fragment]: 85 | return self.event.message.elements # type: ignore 86 | 87 | @property 88 | def content(self) -> str: 89 | return self.event.message.content # type: ignore 90 | 91 | @property 92 | def username(self) -> str: 93 | return self.user.name # type: ignore 94 | 95 | @property 96 | def stripped(self) -> Stripped: 97 | return self._stripped 98 | 99 | def send(self, fragment: Fragment, options: Optional[SendOptions] = None, /): ... 100 | 101 | def cancelQueued(self, delay: float = 1, /): ... 102 | 103 | def sendQueued(self, fragment: Fragment, delay: Optional[float] = None, /): ... 104 | 105 | def getChannel(self, id: Optional[str] = None, fields: Optional[list[str]] = None, /) -> Channel: ... 106 | 107 | def observeChannel(self, fields: Iterable[str], /) -> Channel: ... 108 | 109 | def getUser(self, id: Optional[str] = None, fields: Optional[list[str]] = None, /) -> User: ... 110 | 111 | def observeUser(self, fields: Iterable[str], /) -> User: ... 112 | 113 | def withScope(self, scope: str, callback: Callable[..., str]) -> str: ... 114 | 115 | def resolveScope(self, scope: str, /) -> str: ... 116 | 117 | def text(self, path: Union[str, list[str]], params: Optional[dict[str, Any]] = None, /) -> str: ... 118 | 119 | def i18n( 120 | self, path: Union[str, list[str]], params: Optional[dict[str, Any]] = None, / 121 | ) -> list[Fragment]: ... 122 | 123 | def execute(self, content: str, next: Union[Literal[True], Callable[..., Any]], /): ... 124 | 125 | def middleware(self, middleware: Callable[["Session", Callable[..., Any]], Any], /): ... 126 | 127 | @overload 128 | def prompt(self, timeout: Optional[float] = None, /) -> str: ... 129 | @overload 130 | def prompt(self, callback: Callable[["Session"], T], options: Optional[PromptOptions] = None, /) -> T: ... 131 | def prompt(self, *args) -> Any: ... 132 | 133 | def suggest(self, options: SuggestOptions): ... 134 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | .pdm-python 112 | 113 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 114 | __pypackages__/ 115 | 116 | # Celery stuff 117 | celerybeat-schedule 118 | celerybeat.pid 119 | 120 | # SageMath parsed files 121 | *.sage.py 122 | 123 | # Environments 124 | .env 125 | .venv 126 | env/ 127 | venv/ 128 | ENV/ 129 | env.bak/ 130 | venv.bak/ 131 | 132 | # Spyder project settings 133 | .spyderproject 134 | .spyproject 135 | 136 | # Rope project settings 137 | .ropeproject 138 | 139 | # mkdocs documentation 140 | /site 141 | 142 | # mypy 143 | .mypy_cache/ 144 | .dmypy.json 145 | dmypy.json 146 | 147 | # Pyre type checker 148 | .pyre/ 149 | 150 | # pytype static type analyzer 151 | .pytype/ 152 | 153 | # Cython debug symbols 154 | cython_debug/ 155 | 156 | # PyCharm 157 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 158 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 159 | # and can be added to the global gitignore or merged into this file. For a more nuclear 160 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 161 | .idea/ 162 | 163 | # Logs 164 | logs 165 | npm-debug.log* 166 | yarn-debug.log* 167 | yarn-error.log* 168 | lerna-debug.log* 169 | .pnpm-debug.log* 170 | 171 | # Diagnostic reports (https://nodejs.org/api/report.html) 172 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 173 | 174 | # Runtime data 175 | pids 176 | *.pid 177 | *.seed 178 | *.pid.lock 179 | 180 | # Directory for instrumented libs generated by jscoverage/JSCover 181 | lib-cov 182 | 183 | # Coverage directory used by tools like istanbul 184 | coverage 185 | *.lcov 186 | 187 | # nyc test coverage 188 | .nyc_output 189 | 190 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 191 | .grunt 192 | 193 | # Bower dependency directory (https://bower.io/) 194 | bower_components 195 | 196 | # node-waf configuration 197 | .lock-wscript 198 | 199 | # Compiled binary addons (https://nodejs.org/api/addons.html) 200 | build/Release 201 | 202 | # Dependency directories 203 | node_modules/ 204 | jspm_packages/ 205 | 206 | # Snowpack dependency directory (https://snowpack.dev/) 207 | web_modules/ 208 | 209 | # TypeScript cache 210 | *.tsbuildinfo 211 | 212 | # Optional npm cache directory 213 | .npm 214 | 215 | # Optional eslint cache 216 | .eslintcache 217 | 218 | # Optional stylelint cache 219 | .stylelintcache 220 | 221 | # Microbundle cache 222 | .rpt2_cache/ 223 | .rts2_cache_cjs/ 224 | .rts2_cache_es/ 225 | .rts2_cache_umd/ 226 | 227 | # Optional REPL history 228 | .node_repl_history 229 | 230 | # Output of 'npm pack' 231 | *.tgz 232 | 233 | # Yarn Integrity file 234 | .yarn-integrity 235 | 236 | # dotenv environment variable files 237 | .env.development.local 238 | .env.test.local 239 | .env.production.local 240 | .env.local 241 | 242 | # parcel-bundler cache (https://parceljs.org/) 243 | .parcel-cache 244 | 245 | # Next.js build output 246 | .next 247 | out 248 | 249 | # Nuxt.js build / generate output 250 | .nuxt 251 | dist 252 | 253 | # Gatsby files 254 | .cache/ 255 | # Comment in the public line in if your project uses Gatsby and not Next.js 256 | # https://nextjs.org/blog/next-9-1#public-directory-support 257 | # public 258 | 259 | # vuepress build output 260 | .vuepress/dist 261 | 262 | # vuepress v2.x temp and cache directory 263 | .temp 264 | 265 | # Docusaurus cache and generated files 266 | .docusaurus 267 | 268 | # Serverless directories 269 | .serverless/ 270 | 271 | # FuseBox cache 272 | .fusebox/ 273 | 274 | # DynamoDB Local files 275 | .dynamodb/ 276 | 277 | # TernJS port file 278 | .tern-port 279 | 280 | # Stores VSCode versions used for testing VSCode extensions 281 | .vscode-test 282 | 283 | # yarn v2 284 | .yarn/cache 285 | .yarn/unplugged 286 | .yarn/build-state.yml 287 | .yarn/install-state.gz 288 | .pnp.* 289 | /package.json 290 | /package-lock.json 291 | -------------------------------------------------------------------------------- /src/koishi/context.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Any, Callable, ClassVar, Literal, Optional, Protocol, TypedDict, TypeVar, Union, overload 3 | from typing_extensions import TypeAlias 4 | 5 | import javascript 6 | from loguru import logger 7 | 8 | from .bot import Bot 9 | from .command import Command, CommandConfig 10 | 11 | T = TypeVar("T") 12 | S = TypeVar("S") 13 | 14 | 15 | class Inject(Protocol): 16 | required: Optional[list[str]] 17 | optional: Optional[list[str]] 18 | 19 | 20 | class PluginBase(Protocol[T]): 21 | name: Optional[str] 22 | reactive: Optional[str] 23 | reusable: Optional[str] 24 | Config: Optional[Callable[[Any], T]] 25 | inject: Optional[Union[list[str], Inject]] 26 | 27 | 28 | class PluginTransform(Protocol[S, T]): 29 | schema: Literal[True] 30 | Config: Callable[[S], T] 31 | 32 | 33 | C = TypeVar("C", bound="Context", contravariant=True) 34 | 35 | 36 | class PluginFunction(PluginBase[T], Protocol[C, T]): 37 | def __call__(self, ctx: C, config: T) -> None: ... 38 | 39 | 40 | C1 = TypeVar("C1", bound="Context", covariant=True) 41 | 42 | 43 | class PluginInit(PluginBase[T], Protocol[C1, T]): 44 | def __init__(self, ctx: C1, config: T) -> None: ... 45 | 46 | 47 | class PluginObject(PluginBase[T], Protocol[C, T]): 48 | def apply(self, ctx: C, config: T) -> None: ... 49 | 50 | 51 | class PluginTransformFunction(PluginFunction[C, T], PluginTransform[S, T], Protocol[C, S, T]): 52 | pass 53 | 54 | 55 | class PluginTransformInit(PluginInit[C1, T], PluginTransform[S, T], Protocol[C1, S, T]): 56 | pass 57 | 58 | 59 | class PluginTransformObject(PluginObject[C, T], PluginTransform[S, T], Protocol[C, S, T]): 60 | pass 61 | 62 | 63 | class PluginDictObject(TypedDict): 64 | apply: Callable[["Context", Any], None] 65 | 66 | 67 | PluginType: TypeAlias = Union[ 68 | PluginTransformFunction["Context", None, None], 69 | PluginTransformInit["Context", None, None], 70 | PluginTransformObject["Context", None, None], 71 | PluginFunction["Context", None], 72 | PluginInit["Context", None], 73 | PluginObject["Context", None], 74 | PluginDictObject, 75 | ] 76 | 77 | 78 | class Context: 79 | __context_type__: ClassVar[javascript.proxy.Proxy] 80 | 81 | def __init__(self, config: Any): 82 | self._obj = self.__class__.__context_type__(config) 83 | 84 | @property 85 | def root(self): 86 | if self._obj.root == self._obj: 87 | return self 88 | return Context(self._obj.root) 89 | 90 | @property 91 | def config(self): 92 | return self._obj.config 93 | 94 | @property 95 | def bots(self) -> dict[Union[int, str], Bot[Any]]: 96 | return self._obj.bots 97 | 98 | def inject(self, deps: Union[list[str], Inject], callback: PluginFunction["Context", None]): 99 | self._obj.inject(deps, callback) 100 | 101 | def require(self, name: str, config: Optional[dict[str, Any]] = None, version: Optional[str] = None): 102 | plugin = javascript.require(name, version) 103 | if not plugin.apply: 104 | self._obj.plugin(plugin["default"], config) 105 | else: 106 | self._obj.plugin(plugin, config) 107 | 108 | def requires(self, *names: str, config: Optional[dict[str, dict[str, Any]]] = None): 109 | for name in names: 110 | self.require(name, config.get(name) if config else None) 111 | 112 | def plugin(self, plugin: PluginType, config: Optional[T] = None): 113 | if isinstance(plugin, dict): 114 | self._obj.plugin(plugin) 115 | else: 116 | self._obj.plugin(plugin, config) 117 | 118 | @overload 119 | def on(self, name: str, callback: Callable[..., Any], prepend: bool = False) -> Callable[..., bool]: ... 120 | 121 | @overload 122 | def on( 123 | self, name: str, *, prepend: bool = False 124 | ) -> Callable[[Callable[..., Any]], Callable[..., bool]]: ... 125 | 126 | def on(self, name: str, callback: Optional[Callable[..., Any]] = None, prepend: Optional[bool] = None): 127 | if callback: 128 | return self._obj.on(name, callback, prepend) 129 | 130 | def wrapper(fn: Callable[..., Any]): 131 | return self._obj.on(name, fn, prepend) 132 | 133 | return wrapper 134 | 135 | def emit(self, name: str, *args: Any): 136 | return self._obj.emit(name, *args) 137 | 138 | @overload 139 | def command(self, cmd: str, config: Optional[CommandConfig] = None, /) -> Command: ... 140 | @overload 141 | def command(self, cmd: str, desc: str, config: Optional[CommandConfig] = None, /) -> Command: ... 142 | 143 | def command(self, cmd: str, *args): 144 | return self._obj.command(cmd, *args) 145 | 146 | def start(self): 147 | self._obj.start() 148 | 149 | def stop(self): 150 | self._obj.stop() 151 | 152 | async def daemon(self): 153 | self.start() 154 | while True: 155 | await asyncio.sleep(0.1) 156 | 157 | async def quit(self): 158 | self.stop() 159 | 160 | async def _main(self): 161 | await self.daemon() 162 | await self.quit() 163 | 164 | def run(self): 165 | loop = asyncio.events.new_event_loop() 166 | try: 167 | asyncio.events.set_event_loop(loop) 168 | return loop.run_until_complete(self._main()) 169 | except KeyboardInterrupt: 170 | logger.warning("Interrupt detected, stopping ...") 171 | finally: 172 | try: 173 | to_cancel = asyncio.tasks.all_tasks(loop) 174 | if not to_cancel: 175 | return 176 | 177 | for task in to_cancel: 178 | task.cancel() 179 | 180 | loop.run_until_complete(asyncio.gather(*to_cancel, loop=loop, return_exceptions=True)) 181 | 182 | for task in to_cancel: 183 | if task.cancelled(): 184 | continue 185 | if task.exception() is not None: 186 | loop.call_exception_handler( 187 | { 188 | "message": "unhandled exception during asyncio.run() shutdown", 189 | "exception": task.exception(), 190 | "task": task, 191 | } 192 | ) 193 | loop.run_until_complete(loop.shutdown_asyncgens()) 194 | loop.run_until_complete(loop.shutdown_default_executor()) 195 | finally: 196 | asyncio.events.set_event_loop(None) 197 | loop.close() 198 | -------------------------------------------------------------------------------- /src/koishi/bot.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, Any, Generic, Iterable, Optional, TypeVar, Union 2 | from typing_extensions import Self 3 | 4 | from .element import Fragment 5 | from .models import Channel, Guild, Login, LoginStatus, Member, MessageObject, PageResult, Role, User 6 | 7 | if TYPE_CHECKING: 8 | from .adapter import Adapter 9 | from .context import Context 10 | from .session import SendOptions, Session 11 | 12 | T = TypeVar("T") 13 | 14 | 15 | class Bot(Generic[T]): 16 | user: User 17 | isBot: bool = True 18 | hidden: bool = False 19 | platform: str 20 | selfId: str 21 | adapter: "Adapter[Self]" 22 | error: Optional[Exception] = None 23 | callbacks: dict[str, Any] 24 | config: T 25 | 26 | _status: LoginStatus 27 | 28 | def __init__(self, ctx: "Context", config: T, platform: Optional[str] = None): 29 | self.internal = None 30 | self.context = ctx 31 | self.config = config 32 | if platform: 33 | self.platform = platform 34 | raise NotImplementedError("Bot is an abstract class") 35 | 36 | def update(self, login: Login): ... 37 | 38 | def dispose(self): ... 39 | 40 | @property 41 | def status(self) -> LoginStatus: 42 | return self._status 43 | 44 | @status.setter 45 | def status(self, value: LoginStatus): 46 | self._status = value 47 | 48 | @property 49 | def isActive(self) -> bool: 50 | return self.status != LoginStatus.OFFLINE and self.status != LoginStatus.DISCONNECT 51 | 52 | def online(self): ... 53 | 54 | def offline(self): ... 55 | 56 | def start(self): ... 57 | 58 | def stop(self): ... 59 | 60 | @property 61 | def sid(self) -> str: 62 | return f"{self.platform}:{self.selfId}" 63 | 64 | def session(self, event: dict) -> "Session": ... 65 | 66 | def dispatch(self, session: "Session"): ... 67 | 68 | def createMessage( 69 | self, 70 | channelId: str, 71 | content: Fragment, 72 | guildId: Optional[str] = None, 73 | options: Optional["SendOptions"] = None, 74 | /, 75 | ) -> list[MessageObject]: ... 76 | 77 | def sendMessages( 78 | self, 79 | channelId: str, 80 | content: Fragment, 81 | guildId: Optional[str] = None, 82 | options: Optional["SendOptions"] = None, 83 | /, 84 | ) -> list[str]: ... 85 | 86 | def sendPrivateMessage( 87 | self, 88 | userId: str, 89 | content: Fragment, 90 | guildId: Optional[str] = None, 91 | options: Optional["SendOptions"] = None, 92 | /, 93 | ) -> list[str]: ... 94 | 95 | def checkPermission(self, name: str, session: "Session") -> bool: ... 96 | 97 | def getMessage(self, channelId: str, messageId: str, /) -> MessageObject: ... 98 | 99 | def getMessageList(self, channelId: str, next: Optional[str] = None, /) -> PageResult[MessageObject]: ... 100 | 101 | def getMessageIter(self, channelId: str, /) -> Iterable[MessageObject]: ... 102 | 103 | def editMessage(self, channelId: str, messageId: str, content: Fragment, /) -> None: ... 104 | 105 | def deleteMessage(self, channelId: str, messageId: str, /) -> None: ... 106 | 107 | def createReaction(self, channelId: str, messageId: str, emoji: str, /) -> None: ... 108 | 109 | def deleteReaction( 110 | self, channelId: str, messageId: str, emoji: str, userId: Optional[str] = None, / 111 | ) -> None: ... 112 | 113 | def clearReactions(self, channelId: str, messageId: str, emoji: Optional[str] = None, /) -> None: ... 114 | 115 | def getReactionList( 116 | self, channelId: str, messageId: str, emoji: str, next: Optional[str] = None, / 117 | ) -> PageResult[User]: ... 118 | 119 | def getReactionIter(self, channelId: str, messageId: str, emoji: str, /) -> Iterable[User]: ... 120 | 121 | def getLogin(self) -> Login: ... 122 | 123 | def getUser(self, userId: str, guildId: Optional[str] = None, /) -> User: ... 124 | 125 | def getFriendList(self, next: Optional[str] = None, /) -> PageResult[User]: ... 126 | 127 | def getFriendIter(self, /) -> Iterable[User]: ... 128 | 129 | def deleteFriend(self, userId: str, /) -> None: ... 130 | 131 | def getGuild(self, guildId: str, /) -> Guild: ... 132 | 133 | def getGuildList(self, next: Optional[str] = None, /) -> PageResult[Guild]: ... 134 | 135 | def getGuildIter(self, /) -> Iterable[Guild]: ... 136 | 137 | def getGuildMember(self, guildId: str, userId: str, /) -> Member: ... 138 | 139 | def getGuildMemberList(self, guildId: str, next: Optional[str] = None, /) -> PageResult[Member]: ... 140 | 141 | def getGuildMemberIter(self, guildId: str, /) -> Iterable[Member]: ... 142 | 143 | def kickGuildMember(self, guildId: str, userId: str, permanent: bool = False, /) -> None: ... 144 | 145 | def muteGuildMember( 146 | self, guildId: str, userId: str, duration: float, reason: Optional[str] = None, / 147 | ) -> None: ... 148 | 149 | def setGuildMemberRole(self, guildId: str, userId: str, roleId: str, /) -> None: ... 150 | 151 | def unsetGuildMemberRole(self, guildId: str, userId: str, roleId: str, /) -> None: ... 152 | 153 | def getGuildRoleList(self, guildId: str, next: Optional[str] = None, /) -> PageResult[Role]: ... 154 | 155 | def getGuildRoleIter(self, guildId: str, /) -> Iterable[Role]: ... 156 | 157 | def createGuildRole(self, guildId: str, data: dict, /) -> Role: ... 158 | 159 | def updateGuildRole(self, guildId: str, roleId: str, data: dict, /) -> Role: ... 160 | 161 | def deleteGuildRole(self, guildId: str, roleId: str, /) -> None: ... 162 | 163 | def getChannel(self, channelId: str, /) -> Channel: ... 164 | 165 | def getChannelList( 166 | self, guildId: Optional[str] = None, next: Optional[str] = None, / 167 | ) -> PageResult[Channel]: ... 168 | 169 | def getChannelIter(self, guildId: Optional[str] = None, /) -> Iterable[Channel]: ... 170 | 171 | def createDirectChannel(self, userId: str, guildId: Optional[str] = None, /) -> Channel: ... 172 | 173 | def createChannel(self, guildId: str, data: dict, /) -> Channel: ... 174 | 175 | def updateChannel(self, channelId: str, data: dict, /) -> Channel: ... 176 | 177 | def deleteChannel(self, channelId: str, /) -> None: ... 178 | 179 | def muteChannel(self, channelId: str, guildId: Optional[str] = None, enable: bool = True, /) -> None: ... 180 | 181 | def handleFriendRequest( 182 | self, messageId: str, approve: bool, comment: Optional[str] = None, / 183 | ) -> None: ... 184 | 185 | def handleGuildRequest(self, messageId: str, approve: bool, comment: Optional[str] = None, /) -> None: ... 186 | 187 | def handleGuildMemberRequest( 188 | self, messageId: str, approve: bool, comment: Optional[str] = None, / 189 | ) -> None: ... 190 | 191 | def broadcast( 192 | self, 193 | channels: list[Union[str, tuple[str, str], "Session"]], 194 | content: Fragment, 195 | delay: Optional[float] = None, 196 | /, 197 | ) -> list[str]: ... 198 | -------------------------------------------------------------------------------- /pdm.lock: -------------------------------------------------------------------------------- 1 | # This file is @generated by PDM. 2 | # It is not intended for manual editing. 3 | 4 | [metadata] 5 | groups = ["default", "dev"] 6 | strategy = ["cross_platform", "inherit_metadata"] 7 | lock_version = "4.4.1" 8 | content_hash = "sha256:faf86078bdbc9e0c85c9992d59a20c6d167cf059e72f2dd4678a1156a07870f4" 9 | 10 | [[package]] 11 | name = "black" 12 | version = "24.4.0" 13 | requires_python = ">=3.8" 14 | summary = "The uncompromising code formatter." 15 | groups = ["dev"] 16 | dependencies = [ 17 | "click>=8.0.0", 18 | "mypy-extensions>=0.4.3", 19 | "packaging>=22.0", 20 | "pathspec>=0.9.0", 21 | "platformdirs>=2", 22 | "tomli>=1.1.0; python_version < \"3.11\"", 23 | "typing-extensions>=4.0.1; python_version < \"3.11\"", 24 | ] 25 | files = [ 26 | {file = "black-24.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6ad001a9ddd9b8dfd1b434d566be39b1cd502802c8d38bbb1ba612afda2ef436"}, 27 | {file = "black-24.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3a3a092b8b756c643fe45f4624dbd5a389f770a4ac294cf4d0fce6af86addaf"}, 28 | {file = "black-24.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dae79397f367ac8d7adb6c779813328f6d690943f64b32983e896bcccd18cbad"}, 29 | {file = "black-24.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:71d998b73c957444fb7c52096c3843875f4b6b47a54972598741fe9a7f737fcb"}, 30 | {file = "black-24.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8e5537f456a22cf5cfcb2707803431d2feeb82ab3748ade280d6ccd0b40ed2e8"}, 31 | {file = "black-24.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:64e60a7edd71fd542a10a9643bf369bfd2644de95ec71e86790b063aa02ff745"}, 32 | {file = "black-24.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cd5b4f76056cecce3e69b0d4c228326d2595f506797f40b9233424e2524c070"}, 33 | {file = "black-24.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:64578cf99b6b46a6301bc28bdb89f9d6f9b592b1c5837818a177c98525dbe397"}, 34 | {file = "black-24.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f95cece33329dc4aa3b0e1a771c41075812e46cf3d6e3f1dfe3d91ff09826ed2"}, 35 | {file = "black-24.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4396ca365a4310beef84d446ca5016f671b10f07abdba3e4e4304218d2c71d33"}, 36 | {file = "black-24.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44d99dfdf37a2a00a6f7a8dcbd19edf361d056ee51093b2445de7ca09adac965"}, 37 | {file = "black-24.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:21f9407063ec71c5580b8ad975653c66508d6a9f57bd008bb8691d273705adcd"}, 38 | {file = "black-24.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6644f97a7ef6f401a150cca551a1ff97e03c25d8519ee0bbc9b0058772882665"}, 39 | {file = "black-24.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:75a2d0b4f5eb81f7eebc31f788f9830a6ce10a68c91fbe0fade34fff7a2836e6"}, 40 | {file = "black-24.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb949f56a63c5e134dfdca12091e98ffb5fd446293ebae123d10fc1abad00b9e"}, 41 | {file = "black-24.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:7852b05d02b5b9a8c893ab95863ef8986e4dda29af80bbbda94d7aee1abf8702"}, 42 | {file = "black-24.4.0-py3-none-any.whl", hash = "sha256:74eb9b5420e26b42c00a3ff470dc0cd144b80a766128b1771d07643165e08d0e"}, 43 | {file = "black-24.4.0.tar.gz", hash = "sha256:f07b69fda20578367eaebbd670ff8fc653ab181e1ff95d84497f9fa20e7d0641"}, 44 | ] 45 | 46 | [[package]] 47 | name = "click" 48 | version = "8.1.7" 49 | requires_python = ">=3.7" 50 | summary = "Composable command line interface toolkit" 51 | groups = ["dev"] 52 | dependencies = [ 53 | "colorama; platform_system == \"Windows\"", 54 | ] 55 | files = [ 56 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, 57 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, 58 | ] 59 | 60 | [[package]] 61 | name = "colorama" 62 | version = "0.4.6" 63 | requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 64 | summary = "Cross-platform colored terminal text." 65 | groups = ["default", "dev"] 66 | marker = "platform_system == \"Windows\" or sys_platform == \"win32\"" 67 | files = [ 68 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 69 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 70 | ] 71 | 72 | [[package]] 73 | name = "isort" 74 | version = "5.13.2" 75 | requires_python = ">=3.8.0" 76 | summary = "A Python utility / library to sort Python imports." 77 | groups = ["dev"] 78 | files = [ 79 | {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, 80 | {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, 81 | ] 82 | 83 | [[package]] 84 | name = "javascript" 85 | version = "1!1.1.3" 86 | requires_python = "<4,>=3.7" 87 | git = "https://github.com/koishipy/JSPyBridge" 88 | ref = "ref_val" 89 | revision = "43076ca7c948fb358c03cbdcc8de3766ee7e9390" 90 | summary = "Call and interop Node.js APIs with Python" 91 | groups = ["default"] 92 | dependencies = [ 93 | "loguru>=0.7.2", 94 | ] 95 | 96 | [[package]] 97 | name = "loguru" 98 | version = "0.7.2" 99 | requires_python = ">=3.5" 100 | summary = "Python logging made (stupidly) simple" 101 | groups = ["default"] 102 | dependencies = [ 103 | "colorama>=0.3.4; sys_platform == \"win32\"", 104 | "win32-setctime>=1.0.0; sys_platform == \"win32\"", 105 | ] 106 | files = [ 107 | {file = "loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb"}, 108 | {file = "loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac"}, 109 | ] 110 | 111 | [[package]] 112 | name = "mypy-extensions" 113 | version = "1.0.0" 114 | requires_python = ">=3.5" 115 | summary = "Type system extensions for programs checked with the mypy type checker." 116 | groups = ["dev"] 117 | files = [ 118 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 119 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 120 | ] 121 | 122 | [[package]] 123 | name = "packaging" 124 | version = "24.0" 125 | requires_python = ">=3.7" 126 | summary = "Core utilities for Python packages" 127 | groups = ["dev"] 128 | files = [ 129 | {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, 130 | {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, 131 | ] 132 | 133 | [[package]] 134 | name = "pathspec" 135 | version = "0.12.1" 136 | requires_python = ">=3.8" 137 | summary = "Utility library for gitignore style pattern matching of file paths." 138 | groups = ["dev"] 139 | files = [ 140 | {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, 141 | {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, 142 | ] 143 | 144 | [[package]] 145 | name = "platformdirs" 146 | version = "4.2.0" 147 | requires_python = ">=3.8" 148 | summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 149 | groups = ["dev"] 150 | files = [ 151 | {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, 152 | {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, 153 | ] 154 | 155 | [[package]] 156 | name = "ruff" 157 | version = "0.3.7" 158 | requires_python = ">=3.7" 159 | summary = "An extremely fast Python linter and code formatter, written in Rust." 160 | groups = ["dev"] 161 | files = [ 162 | {file = "ruff-0.3.7-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0e8377cccb2f07abd25e84fc5b2cbe48eeb0fea9f1719cad7caedb061d70e5ce"}, 163 | {file = "ruff-0.3.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:15a4d1cc1e64e556fa0d67bfd388fed416b7f3b26d5d1c3e7d192c897e39ba4b"}, 164 | {file = "ruff-0.3.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d28bdf3d7dc71dd46929fafeec98ba89b7c3550c3f0978e36389b5631b793663"}, 165 | {file = "ruff-0.3.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:379b67d4f49774ba679593b232dcd90d9e10f04d96e3c8ce4a28037ae473f7bb"}, 166 | {file = "ruff-0.3.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c060aea8ad5ef21cdfbbe05475ab5104ce7827b639a78dd55383a6e9895b7c51"}, 167 | {file = "ruff-0.3.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ebf8f615dde968272d70502c083ebf963b6781aacd3079081e03b32adfe4d58a"}, 168 | {file = "ruff-0.3.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d48098bd8f5c38897b03604f5428901b65e3c97d40b3952e38637b5404b739a2"}, 169 | {file = "ruff-0.3.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da8a4fda219bf9024692b1bc68c9cff4b80507879ada8769dc7e985755d662ea"}, 170 | {file = "ruff-0.3.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c44e0149f1d8b48c4d5c33d88c677a4aa22fd09b1683d6a7ff55b816b5d074f"}, 171 | {file = "ruff-0.3.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3050ec0af72b709a62ecc2aca941b9cd479a7bf2b36cc4562f0033d688e44fa1"}, 172 | {file = "ruff-0.3.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a29cc38e4c1ab00da18a3f6777f8b50099d73326981bb7d182e54a9a21bb4ff7"}, 173 | {file = "ruff-0.3.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b15cc59c19edca917f51b1956637db47e200b0fc5e6e1878233d3a938384b0b"}, 174 | {file = "ruff-0.3.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e491045781b1e38b72c91247cf4634f040f8d0cb3e6d3d64d38dcf43616650b4"}, 175 | {file = "ruff-0.3.7-py3-none-win32.whl", hash = "sha256:bc931de87593d64fad3a22e201e55ad76271f1d5bfc44e1a1887edd0903c7d9f"}, 176 | {file = "ruff-0.3.7-py3-none-win_amd64.whl", hash = "sha256:5ef0e501e1e39f35e03c2acb1d1238c595b8bb36cf7a170e7c1df1b73da00e74"}, 177 | {file = "ruff-0.3.7-py3-none-win_arm64.whl", hash = "sha256:789e144f6dc7019d1f92a812891c645274ed08af6037d11fc65fcbc183b7d59f"}, 178 | {file = "ruff-0.3.7.tar.gz", hash = "sha256:d5c1aebee5162c2226784800ae031f660c350e7a3402c4d1f8ea4e97e232e3ba"}, 179 | ] 180 | 181 | [[package]] 182 | name = "tomli" 183 | version = "2.0.1" 184 | requires_python = ">=3.7" 185 | summary = "A lil' TOML parser" 186 | groups = ["dev"] 187 | marker = "python_version < \"3.11\"" 188 | files = [ 189 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 190 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 191 | ] 192 | 193 | [[package]] 194 | name = "typing-extensions" 195 | version = "4.11.0" 196 | requires_python = ">=3.8" 197 | summary = "Backported and Experimental Type Hints for Python 3.8+" 198 | groups = ["default", "dev"] 199 | files = [ 200 | {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, 201 | {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, 202 | ] 203 | 204 | [[package]] 205 | name = "win32-setctime" 206 | version = "1.1.0" 207 | requires_python = ">=3.5" 208 | summary = "A small Python utility to set file creation time on Windows" 209 | groups = ["default"] 210 | marker = "sys_platform == \"win32\"" 211 | files = [ 212 | {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, 213 | {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, 214 | ] 215 | --------------------------------------------------------------------------------