├── .github └── workflows │ └── release.yml ├── .gitignore ├── README.md ├── example └── first-one │ ├── .gitignore │ ├── main.py │ ├── modules │ ├── __init__.py │ ├── module_as_dir │ │ └── __init__.py │ └── module_as_file.py │ ├── poetry.lock │ └── pyproject.toml ├── pdm.lock ├── pyproject.toml ├── src └── graia │ └── saya │ ├── __init__.py │ ├── behaviour │ ├── __init__.py │ ├── context.py │ ├── entity.py │ └── interface.py │ ├── builtins │ ├── __init__.py │ └── broadcast │ │ ├── __init__.py │ │ ├── behaviour.py │ │ ├── schema.py │ │ └── shortcut.py │ ├── channel.py │ ├── context.py │ ├── creator.py │ ├── cube.py │ ├── event.py │ ├── factory.py │ └── schema.py └── test.py /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Python package 2 | on: 3 | push: 4 | tags: 5 | - "v*.*.*" 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | 12 | - name: Install PDM 13 | uses: pdm-project/setup-pdm@main 14 | 15 | - name: Build Package 16 | run: pdm build 17 | 18 | - name: Publish to PyPI 19 | uses: pypa/gh-action-pypi-publish@release/v1 20 | with: 21 | password: ${{ secrets.PYPI_TOKEN }} 22 | skip_existing: true 23 | -------------------------------------------------------------------------------- /.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-python 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # Saya 4 | 5 | _a modular implementation with modern design and injection_ 6 | 7 | > 不仅仅是模块化, 就如炭之魔女不仅仅只有追求心上人的一面. 8 | > 9 | 10 |
11 | 12 | ## Installation 13 | 14 | ```bash 15 | pip install graia-saya 16 | ``` 17 | 18 | 或者使用 `poetry` : 19 | 20 | ```bash 21 | poetry add graia-saya 22 | ``` 23 | 24 | ## 架构简述 25 | 26 | Saya 的架构分为这几大块: `Saya Controller` (控制器), `Module Channel` (模块容器), `Cube` (内容容器), `Schema` (元信息模板), `Behaviour` (行为). 27 | 28 | - `Saya Controller` : 负责控制各个模块, 分配 `Channel` , 管理模块启停, `Behaviour` 的注册和调用. 29 | - `Module Channel` : 负责对模块服务, 收集模块的各式信息, 像 模块的名称, 作者, 长段的描述 之类, 30 | 并负责包装模块的内容为 `Cube` , 用以 `Behaviour` 对底层接口的操作. 31 | - `Cube` : 对模块提供的内容附加一个由 `Schema` 实例化来的 `metadata` , 即 "元信息", 用于给 `Behaviour` 进行处理. 32 | - `Schema` : 用于给模块提供的内容附加不同类型的元信息, 给 `Behaviour` `isinstance` 处理用. 33 | - `Behaviour` : 根据 `Cube` 及其元信息, 对底层接口(例如 `Broadcast` , `Scheduler` 等)进行操作. 34 | 包括 `allocate` 与 `uninstall` 两个操作. 35 | 36 | ## 使用 37 | 38 | 安装后, 在编辑器内打开工作区, 创建如下的目录结构: 39 | 40 | 这里我们建立的是 Real World 中的, 最简且最易于扩展, 维护的 **示例性** 目录结构. 41 | `Saya` 中, 我们的导入机制复用了 Python 自身的模块和包机制, 理论上只需要符合 Python 的导入规则, 42 | 就能引入模块到实例中. 43 | 44 | ```bash 45 | /home/elaina/saya-example 46 | │ .gitignore 47 | │ main.py 48 | │ pyproject.toml 49 | │ 50 | └─ modules 51 | │ __init__.py 52 | │ module_as_file.py # 作为文件的合法模块可以被调用. 53 | │ 54 | └─ module_as_dir # 作为文件夹的合法模块可以被调用(仅调用 __init__.py 下的内容). 55 | __init__.py 56 | ``` 57 | 58 | `Saya` 需要一个入口( `entry` ), 用于创建 `Controller` , 并让 `Controller` 分配 `Channel` 给这之后被 `Saya.require` 方法引入的模块. 59 | 在我们提供的目录结构中, `main.py` 将作为入口文件, 被 Python 解释器首先执行. 60 | 61 | ### 入口文件的编写 62 | 63 | 首先, 我们需要引入 `Saya` , `Broadcast` , 还有其内部集成的对 `Broadcast` 的支持: 64 | 65 | ```py 66 | from graia.saya import Saya 67 | from graia.broadcast import Broadcast 68 | from graia.saya.builtins.broadcast import BroadcastBehaviour 69 | ``` 70 | 71 | 分别创建 `Broadcast` , `Saya` 的实例: 72 | 73 | ```py 74 | import asyncio 75 | 76 | loop = asyncio.get_event_loop() 77 | broadcast = Broadcast(loop=loop) 78 | saya = Saya(broadcast) # 这里可以置空, 但是会丢失 Lifecycle 特性 79 | ``` 80 | 81 | 创建 `BroadcastBehaviour` 的实例, 并将其注册到现有的 `Saya` 实例中: 82 | 83 | ```py 84 | saya.install_behaviours(BroadcastBehaviour(broadcast)) 85 | ``` 86 | 87 | 为了导入各个模块, `Saya Controller` 需要先进入上下文: 88 | 89 | ```py 90 | with saya.module_context(): 91 | ... 92 | ``` 93 | 94 | 引入各个模块, 这里的模块目前都需要手动引入, 后期可能会加入配置系统: 95 | 96 | ```py 97 | with saya.module_context(): 98 | saya.require("modules.module_as_file") 99 | saya.require("modules.module_as_dir") 100 | ``` 101 | 102 | 这里使用传统方式启动 `asyncio` 的事件循环. 103 | 104 | > 不同框架有不同的启动方式, 比如 Avilla 使用了 Launch Component, 这里仅做演示. 105 | 106 | ```py 107 | try: 108 | loop.run_forever() 109 | except KeyboardInterrupt: 110 | exit() 111 | ``` 112 | 113 | 或者也可以这样: 114 | 115 | ```py 116 | async def do_nothing(): 117 | pass 118 | 119 | loop.run_until_complete(do_nothing()) 120 | ``` 121 | 122 | 最终的结果: 123 | 124 | ```py title="Result of main.py" 125 | import asyncio 126 | 127 | from graia.saya import Saya 128 | from graia.broadcast import Broadcast 129 | from graia.saya.builtins.broadcast import BroadcastBehaviour 130 | 131 | loop = asyncio.get_event_loop() 132 | broadcast = Broadcast(loop=loop) 133 | saya = Saya(broadcast) 134 | saya.install_behaviours(BroadcastBehaviour(broadcast)) 135 | 136 | with saya.module_context(): 137 | 138 | saya.require("modules.module_as_file") 139 | saya.require("modules.module_as_dir") 140 | 141 | try: 142 | 143 | loop.run_forever() 144 | 145 | except KeyboardInterrupt: 146 | 147 | exit() 148 | 149 | ``` 150 | 151 | 就这样, 一个入口文件就这样完成了, 现在主要是插件部分. 152 | 153 | ## 第一次 154 | 155 | 来到 `module_as_file.py`: 156 | 157 | ```py 158 | from graia.saya import Saya, Channel 159 | 160 | saya = Saya.current() 161 | channel = Channel.current() 162 | ``` 163 | 164 | 两个 `currnet` 方法的调用, 访问了 `Saya` 实例和当前上下文分配的 `Channel` . 165 | 166 | 接下来, 导入 `ListenerSchema` : 167 | 168 | ```py 169 | from graia.saya.builtins.broadcast.schema import ListenerSchema 170 | ``` 171 | 172 | `ListenerSchema` 作为 `Schema` , 标识相对应的模块内容为一 `Listener` , 173 | 并在模块被导入后经由 `Behaviour` 进行操作. 174 | 175 | 使用 `Channel.use` 方法, 向 `Channel` 提供内容: 176 | 177 | ```py 178 | @channel.use(ListenerSchema( 179 | listening_events=[...] # 填入你需要监听的事件 180 | )) 181 | async def module_listener(): 182 | print("事件被触发!!!!") 183 | ``` 184 | 185 | 然后, 引入结束, `module_as_file.py` 文件内容如下, 这里我们监听 `SayaModuleInstalled` 事件, 作为 `Lifecycle API` 的简单示例: 186 | 187 | ```py title="Result of module_as_file.py" 188 | from graia.saya import Saya, Channel 189 | from graia.saya.builtins.broadcast.schema import ListenerSchema 190 | from graia.saya.event import SayaModuleInstalled 191 | 192 | saya = Saya.current() 193 | channel = Channel.current() 194 | 195 | @channel.use(ListenerSchema( 196 | 197 | listening_events=[SayaModuleInstalled] 198 | 199 | )) 200 | async def module_listener(event: SayaModuleInstalled): 201 | 202 | print(f"{event.module}::模块加载成功!!!") 203 | 204 | ``` 205 | 206 | 我们对 `modules/module_as_dir/__init__.py` 也如法炮制, copy 上方的代码, 进入虚拟环境, 然后运行 `main.py`. 207 | 208 | ```bash 209 | root@localhost: # python main.py 210 | 2021-02-16 01:19:56.632 | DEBUG | graia.saya:require:58 - require modules.module_as_file 211 | 2021-02-16 01:19:56.639 | DEBUG | graia.saya:require:58 - require modules.module_as_dir 212 | modules.module_as_file::模块加载成功!!! 213 | modules.module_as_file::模块加载成功!!! 214 | modules.module_as_dir::模块加载成功!!! 215 | modules.module_as_dir::模块加载成功!!! 216 | ``` 217 | 218 | ## Factory 219 | 220 | `saya.factory` 提供了 `factory` 与 `buffer_modifier` 两个装饰器, 用于进一步构建自定义的装饰器来构造用于 `Channel.use` 的 `Schema` . 221 | 222 | 以 `ListenerSchema` 为例: 223 | 224 | ```python 225 | from graia.saya.factory import factory 226 | 227 | @factory 228 | def listen(*event) -> SchemaWrapper: 229 | def wrapper(func: Callable, buffer: Dict[str, Any]) -> ListenerSchema: 230 | buffer["inline_dispatchers"] = buffer.pop("dispatchers", []) 231 | return ListenerSchema(listening_events=list(event), **buffer) 232 | 233 | return wrapper 234 | ``` 235 | 236 | 再将其装饰在响应的函数上: 237 | 238 | ```python 239 | from graia.saya.builtins.broadcast.shortcut import listen 240 | 241 | @listen(...) # 填入你需要监听的事件 242 | async def module_listener(): 243 | print("事件被触发!!!!") 244 | ``` 245 | 246 | ## 协议 247 | 248 | 本项目使用 MIT 作为开源协议. 249 | -------------------------------------------------------------------------------- /example/first-one/.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | pip-wheel-metadata/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | cover/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | # For a library or package, you might want to ignore these files since the code is 88 | # intended to run in multiple environments; otherwise, check them in: 89 | # .python-version 90 | 91 | # pipenv 92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 95 | # install all needed dependencies. 96 | #Pipfile.lock 97 | 98 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 99 | __pypackages__/ 100 | 101 | # Celery stuff 102 | celerybeat-schedule 103 | celerybeat.pid 104 | 105 | # SageMath parsed files 106 | *.sage.py 107 | 108 | # Environments 109 | .env 110 | .venv 111 | env/ 112 | venv/ 113 | ENV/ 114 | env.bak/ 115 | venv.bak/ 116 | 117 | # Spyder project settings 118 | .spyderproject 119 | .spyproject 120 | 121 | # Rope project settings 122 | .ropeproject 123 | 124 | # mkdocs documentation 125 | /site 126 | 127 | # mypy 128 | .mypy_cache/ 129 | .dmypy.json 130 | dmypy.json 131 | 132 | # Pyre type checker 133 | .pyre/ 134 | 135 | # pytype static type analyzer 136 | .pytype/ 137 | 138 | # test 139 | ./test.py -------------------------------------------------------------------------------- /example/first-one/main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from graia.broadcast import Broadcast 4 | 5 | from graia.saya import Saya 6 | from graia.saya.builtins.broadcast import BroadcastBehaviour 7 | 8 | loop = asyncio.get_event_loop() 9 | broadcast = Broadcast(loop=loop) 10 | saya = Saya(broadcast) 11 | saya.install_behaviours(BroadcastBehaviour(broadcast)) 12 | 13 | with saya.module_context(): 14 | saya.require("modules.module_as_file") 15 | saya.require("modules.module_as_dir") 16 | 17 | try: 18 | loop.run_forever() 19 | except KeyboardInterrupt: 20 | exit() 21 | -------------------------------------------------------------------------------- /example/first-one/modules/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GraiaProject/Saya/c7ac639cb0e4c3b95c6db07603b5973d74081f5c/example/first-one/modules/__init__.py -------------------------------------------------------------------------------- /example/first-one/modules/module_as_dir/__init__.py: -------------------------------------------------------------------------------- 1 | from graia.saya import Channel, Saya 2 | from graia.saya.builtins.broadcast.schema import ListenerSchema 3 | from graia.saya.event import SayaModuleInstalled 4 | 5 | saya = Saya.current() 6 | channel = Channel.current() 7 | 8 | 9 | @channel.use(ListenerSchema(listening_events=[SayaModuleInstalled])) 10 | async def module_listener(event: SayaModuleInstalled): 11 | print(f"{event.module}::模块加载成功!!!") 12 | -------------------------------------------------------------------------------- /example/first-one/modules/module_as_file.py: -------------------------------------------------------------------------------- 1 | from graia.saya import Channel, Saya 2 | from graia.saya.builtins.broadcast.schema import ListenerSchema 3 | from graia.saya.event import SayaModuleInstalled 4 | 5 | saya = Saya.current() 6 | channel = Channel.current() 7 | 8 | 9 | @channel.use(ListenerSchema(listening_events=[SayaModuleInstalled])) 10 | async def module_listener(event: SayaModuleInstalled): 11 | print(f"{event.module}::模块加载成功!!!") 12 | -------------------------------------------------------------------------------- /example/first-one/poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "appdirs" 3 | version = "1.4.4" 4 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 5 | category = "dev" 6 | optional = false 7 | python-versions = "*" 8 | 9 | [[package]] 10 | name = "black" 11 | version = "20.8b1" 12 | description = "The uncompromising code formatter." 13 | category = "dev" 14 | optional = false 15 | python-versions = ">=3.6" 16 | 17 | [package.dependencies] 18 | appdirs = "*" 19 | click = ">=7.1.2" 20 | mypy-extensions = ">=0.4.3" 21 | pathspec = ">=0.6,<1" 22 | regex = ">=2020.1.8" 23 | toml = ">=0.10.1" 24 | typed-ast = ">=1.4.0" 25 | typing-extensions = ">=3.7.4" 26 | 27 | [package.extras] 28 | colorama = ["colorama (>=0.4.3)"] 29 | d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] 30 | 31 | [[package]] 32 | name = "click" 33 | version = "7.1.2" 34 | description = "Composable command line interface toolkit" 35 | category = "dev" 36 | optional = false 37 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 38 | 39 | [[package]] 40 | name = "colorama" 41 | version = "0.4.4" 42 | description = "Cross-platform colored terminal text." 43 | category = "main" 44 | optional = false 45 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 46 | 47 | [[package]] 48 | name = "graia-broadcast" 49 | version = "0.7.0" 50 | description = "a highly customizable, elegantly designed event system based on asyncio" 51 | category = "main" 52 | optional = false 53 | python-versions = ">=3.6,<4.0" 54 | 55 | [package.dependencies] 56 | iterwrapper = ">=0.1.2,<0.2.0" 57 | pydantic = ">=1.5.1,<1.7.1" 58 | 59 | [[package]] 60 | name = "graia-saya" 61 | version = "0.0.1" 62 | description = "" 63 | category = "main" 64 | optional = false 65 | python-versions = ">=3.7,<4.0" 66 | 67 | [package.dependencies] 68 | graia-broadcast = ">=0.7.0" 69 | loguru = ">=0.5.3,<0.6.0" 70 | 71 | [[package]] 72 | name = "iterwrapper" 73 | version = "0.1.4" 74 | description = "A wrapper for FP style iterator manipulation" 75 | category = "main" 76 | optional = false 77 | python-versions = ">=3.6" 78 | 79 | [[package]] 80 | name = "loguru" 81 | version = "0.5.3" 82 | description = "Python logging made (stupidly) simple" 83 | category = "main" 84 | optional = false 85 | python-versions = ">=3.5" 86 | 87 | [package.dependencies] 88 | colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} 89 | win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} 90 | 91 | [package.extras] 92 | dev = ["codecov (>=2.0.15)", "colorama (>=0.3.4)", "flake8 (>=3.7.7)", "tox (>=3.9.0)", "tox-travis (>=0.12)", "pytest (>=4.6.2)", "pytest-cov (>=2.7.1)", "Sphinx (>=2.2.1)", "sphinx-autobuild (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "black (>=19.10b0)", "isort (>=5.1.1)"] 93 | 94 | [[package]] 95 | name = "mypy-extensions" 96 | version = "0.4.3" 97 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 98 | category = "dev" 99 | optional = false 100 | python-versions = "*" 101 | 102 | [[package]] 103 | name = "pathspec" 104 | version = "0.8.1" 105 | description = "Utility library for gitignore style pattern matching of file paths." 106 | category = "dev" 107 | optional = false 108 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 109 | 110 | [[package]] 111 | name = "pydantic" 112 | version = "1.7" 113 | description = "Data validation and settings management using python 3.6 type hinting" 114 | category = "main" 115 | optional = false 116 | python-versions = ">=3.6" 117 | 118 | [package.extras] 119 | dotenv = ["python-dotenv (>=0.10.4)"] 120 | email = ["email-validator (>=1.0.3)"] 121 | typing_extensions = ["typing-extensions (>=3.7.2)"] 122 | 123 | [[package]] 124 | name = "regex" 125 | version = "2020.11.13" 126 | description = "Alternative regular expression module, to replace re." 127 | category = "dev" 128 | optional = false 129 | python-versions = "*" 130 | 131 | [[package]] 132 | name = "toml" 133 | version = "0.10.2" 134 | description = "Python Library for Tom's Obvious, Minimal Language" 135 | category = "dev" 136 | optional = false 137 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 138 | 139 | [[package]] 140 | name = "typed-ast" 141 | version = "1.4.2" 142 | description = "a fork of Python 2 and 3 ast modules with type comment support" 143 | category = "dev" 144 | optional = false 145 | python-versions = "*" 146 | 147 | [[package]] 148 | name = "typing-extensions" 149 | version = "3.7.4.3" 150 | description = "Backported and Experimental Type Hints for Python 3.5+" 151 | category = "dev" 152 | optional = false 153 | python-versions = "*" 154 | 155 | [[package]] 156 | name = "win32-setctime" 157 | version = "1.0.3" 158 | description = "A small Python utility to set file creation time on Windows" 159 | category = "main" 160 | optional = false 161 | python-versions = ">=3.5" 162 | 163 | [package.extras] 164 | dev = ["pytest (>=4.6.2)", "black (>=19.3b0)"] 165 | 166 | [metadata] 167 | lock-version = "1.1" 168 | python-versions = "^3.7" 169 | content-hash = "ab555969045b933d22bc3cd915a0aafce384e7e395b21168ca0cc234456406f7" 170 | 171 | [metadata.files] 172 | appdirs = [ 173 | {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, 174 | {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, 175 | ] 176 | black = [ 177 | {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, 178 | ] 179 | click = [ 180 | {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, 181 | {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, 182 | ] 183 | colorama = [ 184 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 185 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 186 | ] 187 | graia-broadcast = [ 188 | {file = "graia-broadcast-0.7.0.tar.gz", hash = "sha256:3f251747023b000ff565eda289a356915442202ba9e74bf247c232b9c940a971"}, 189 | {file = "graia_broadcast-0.7.0-py3-none-any.whl", hash = "sha256:889e49aa8976d22bdce07c307ec6707cee9788064c4348063111717df7125ad9"}, 190 | ] 191 | graia-saya = [ 192 | {file = "graia-saya-0.0.1.tar.gz", hash = "sha256:3171f6739fd55466df57eb1b798c7d6daccc836ae6e9cac5131fd84ca4fcef8b"}, 193 | {file = "graia_saya-0.0.1-py3-none-any.whl", hash = "sha256:b891770afd798a71628cb71be322a04788e749d062cfca604b8e571acf3aabf4"}, 194 | ] 195 | iterwrapper = [ 196 | {file = "iterwrapper-0.1.4-py3-none-any.whl", hash = "sha256:a3296d81fa2e558734f572617487fb6be3e2af12af2776899dd29d2154ffdba5"}, 197 | {file = "iterwrapper-0.1.4.tar.gz", hash = "sha256:f2dd1e5a55935a0ce151912524de1781d2aa9cb1fda3e2424fb403ff857f63d9"}, 198 | ] 199 | loguru = [ 200 | {file = "loguru-0.5.3-py3-none-any.whl", hash = "sha256:f8087ac396b5ee5f67c963b495d615ebbceac2796379599820e324419d53667c"}, 201 | {file = "loguru-0.5.3.tar.gz", hash = "sha256:b28e72ac7a98be3d28ad28570299a393dfcd32e5e3f6a353dec94675767b6319"}, 202 | ] 203 | mypy-extensions = [ 204 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 205 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 206 | ] 207 | pathspec = [ 208 | {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, 209 | {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, 210 | ] 211 | pydantic = [ 212 | {file = "pydantic-1.7-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:fde17f99d1610c9b5129f5b37d9ae6d549e0cc6108df991e2c14c7c99d406a89"}, 213 | {file = "pydantic-1.7-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:caa14909e26c69585628dcc5c97d5a26bcc447eca4baaf3a646a9919ff91ac69"}, 214 | {file = "pydantic-1.7-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:22ed6db2a6d6b40350078eb3bb71e6923e4e994678feda220d28f1c30da0b8da"}, 215 | {file = "pydantic-1.7-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:d024440c27d4d0fb862b279e43d2b3f5c5e5680e0627de8f7ca60e471457032e"}, 216 | {file = "pydantic-1.7-cp36-cp36m-win_amd64.whl", hash = "sha256:afe2cafc41249464ad42cf2302128baa796f037099fc3b9eaa54d1b873529c67"}, 217 | {file = "pydantic-1.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:471ef129991cc2c965bef11eacb4bd5451baa0b60b4bbe1bc47e40a760facb88"}, 218 | {file = "pydantic-1.7-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:e687492a769f13c8c96f8c657720a41137be65b8682a6fce5241e0a37d500bc4"}, 219 | {file = "pydantic-1.7-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:f0747f01aa95f1c561e6e64f8e06433d37ea4ac386519bcaddfd8f18e3ebc6fc"}, 220 | {file = "pydantic-1.7-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:9143f236e1969a501bbedbeb70aa6e85b6940fc84a06effba620620d68c2bee6"}, 221 | {file = "pydantic-1.7-cp37-cp37m-win_amd64.whl", hash = "sha256:1d9a6484f1690c94ee7851f74a75eae41980b9f07b8931e14225c050a5eaa821"}, 222 | {file = "pydantic-1.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1724517b6205ce02be6b8f8bf70ec9cb52b94cf40ab9dec7a7609bcf5254e2d2"}, 223 | {file = "pydantic-1.7-cp38-cp38-manylinux1_i686.whl", hash = "sha256:bc1fca601c7c67231464f0be5594942ec7da9ba3a4ee2e3533f02802a7cc8e65"}, 224 | {file = "pydantic-1.7-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:17c199147b4f1d43f40ee7e735ccaff45f09e90ad54cebff8676bedb4f159634"}, 225 | {file = "pydantic-1.7-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:7d0d84401597734097a9c4681556769168de3d1acfc14fdc5c13e523414f9ecc"}, 226 | {file = "pydantic-1.7-cp38-cp38-win_amd64.whl", hash = "sha256:b6d70d28aef95cebd8761818b4dfeb6bdbb71a68c1f4beb8983f083c630d57ee"}, 227 | {file = "pydantic-1.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d5a131a5be9eee7172b98c680dc26f2382e66071c4848eb310c3cce00aeee4df"}, 228 | {file = "pydantic-1.7-cp39-cp39-manylinux1_i686.whl", hash = "sha256:8bd368a22f6a8dce262f7672c2cf06460a6ab0486fbfa8f6482d325d34277075"}, 229 | {file = "pydantic-1.7-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:8e30ee558b295ef12572d0a9a3a35a2d33115f280e051e0b816c4b2448f0cb63"}, 230 | {file = "pydantic-1.7-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:a3087623f8aa1795ebc3a69eb94683772db0943a4adef1bf5e9553effaafea93"}, 231 | {file = "pydantic-1.7-cp39-cp39-win_amd64.whl", hash = "sha256:7feac9bd1078adf7ae705c8c092b3ea5ada05318b38fd5e708cfce9f167dbeb8"}, 232 | {file = "pydantic-1.7-py3-none-any.whl", hash = "sha256:e43b66bf115860e7ef10efb8dd07a831d57c38df1efe475789c34c55067fb7fd"}, 233 | {file = "pydantic-1.7.tar.gz", hash = "sha256:38ee226f71dfbb7b91bc3d8af9932bf18c7505e57f7ed442e8cb78ff35c006a7"}, 234 | ] 235 | regex = [ 236 | {file = "regex-2020.11.13-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85"}, 237 | {file = "regex-2020.11.13-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a63f1a07932c9686d2d416fb295ec2c01ab246e89b4d58e5fa468089cab44b70"}, 238 | {file = "regex-2020.11.13-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:6e4b08c6f8daca7d8f07c8d24e4331ae7953333dbd09c648ed6ebd24db5a10ee"}, 239 | {file = "regex-2020.11.13-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:bba349276b126947b014e50ab3316c027cac1495992f10e5682dc677b3dfa0c5"}, 240 | {file = "regex-2020.11.13-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:56e01daca75eae420bce184edd8bb341c8eebb19dd3bce7266332258f9fb9dd7"}, 241 | {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:6a8ce43923c518c24a2579fda49f093f1397dad5d18346211e46f134fc624e31"}, 242 | {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:1ab79fcb02b930de09c76d024d279686ec5d532eb814fd0ed1e0051eb8bd2daa"}, 243 | {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:9801c4c1d9ae6a70aeb2128e5b4b68c45d4f0af0d1535500884d644fa9b768c6"}, 244 | {file = "regex-2020.11.13-cp36-cp36m-win32.whl", hash = "sha256:49cae022fa13f09be91b2c880e58e14b6da5d10639ed45ca69b85faf039f7a4e"}, 245 | {file = "regex-2020.11.13-cp36-cp36m-win_amd64.whl", hash = "sha256:749078d1eb89484db5f34b4012092ad14b327944ee7f1c4f74d6279a6e4d1884"}, 246 | {file = "regex-2020.11.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b2f4007bff007c96a173e24dcda236e5e83bde4358a557f9ccf5e014439eae4b"}, 247 | {file = "regex-2020.11.13-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:38c8fd190db64f513fe4e1baa59fed086ae71fa45083b6936b52d34df8f86a88"}, 248 | {file = "regex-2020.11.13-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5862975b45d451b6db51c2e654990c1820523a5b07100fc6903e9c86575202a0"}, 249 | {file = "regex-2020.11.13-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:262c6825b309e6485ec2493ffc7e62a13cf13fb2a8b6d212f72bd53ad34118f1"}, 250 | {file = "regex-2020.11.13-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bafb01b4688833e099d79e7efd23f99172f501a15c44f21ea2118681473fdba0"}, 251 | {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:e32f5f3d1b1c663af7f9c4c1e72e6ffe9a78c03a31e149259f531e0fed826512"}, 252 | {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:3bddc701bdd1efa0d5264d2649588cbfda549b2899dc8d50417e47a82e1387ba"}, 253 | {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:02951b7dacb123d8ea6da44fe45ddd084aa6777d4b2454fa0da61d569c6fa538"}, 254 | {file = "regex-2020.11.13-cp37-cp37m-win32.whl", hash = "sha256:0d08e71e70c0237883d0bef12cad5145b84c3705e9c6a588b2a9c7080e5af2a4"}, 255 | {file = "regex-2020.11.13-cp37-cp37m-win_amd64.whl", hash = "sha256:1fa7ee9c2a0e30405e21031d07d7ba8617bc590d391adfc2b7f1e8b99f46f444"}, 256 | {file = "regex-2020.11.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:baf378ba6151f6e272824b86a774326f692bc2ef4cc5ce8d5bc76e38c813a55f"}, 257 | {file = "regex-2020.11.13-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e3faaf10a0d1e8e23a9b51d1900b72e1635c2d5b0e1bea1c18022486a8e2e52d"}, 258 | {file = "regex-2020.11.13-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2a11a3e90bd9901d70a5b31d7dd85114755a581a5da3fc996abfefa48aee78af"}, 259 | {file = "regex-2020.11.13-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1ebb090a426db66dd80df8ca85adc4abfcbad8a7c2e9a5ec7513ede522e0a8f"}, 260 | {file = "regex-2020.11.13-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:b2b1a5ddae3677d89b686e5c625fc5547c6e492bd755b520de5332773a8af06b"}, 261 | {file = "regex-2020.11.13-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:2c99e97d388cd0a8d30f7c514d67887d8021541b875baf09791a3baad48bb4f8"}, 262 | {file = "regex-2020.11.13-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:c084582d4215593f2f1d28b65d2a2f3aceff8342aa85afd7be23a9cad74a0de5"}, 263 | {file = "regex-2020.11.13-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:a3d748383762e56337c39ab35c6ed4deb88df5326f97a38946ddd19028ecce6b"}, 264 | {file = "regex-2020.11.13-cp38-cp38-win32.whl", hash = "sha256:7913bd25f4ab274ba37bc97ad0e21c31004224ccb02765ad984eef43e04acc6c"}, 265 | {file = "regex-2020.11.13-cp38-cp38-win_amd64.whl", hash = "sha256:6c54ce4b5d61a7129bad5c5dc279e222afd00e721bf92f9ef09e4fae28755683"}, 266 | {file = "regex-2020.11.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1862a9d9194fae76a7aaf0150d5f2a8ec1da89e8b55890b1786b8f88a0f619dc"}, 267 | {file = "regex-2020.11.13-cp39-cp39-manylinux1_i686.whl", hash = "sha256:4902e6aa086cbb224241adbc2f06235927d5cdacffb2425c73e6570e8d862364"}, 268 | {file = "regex-2020.11.13-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7a25fcbeae08f96a754b45bdc050e1fb94b95cab046bf56b016c25e9ab127b3e"}, 269 | {file = "regex-2020.11.13-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:d2d8ce12b7c12c87e41123997ebaf1a5767a5be3ec545f64675388970f415e2e"}, 270 | {file = "regex-2020.11.13-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f7d29a6fc4760300f86ae329e3b6ca28ea9c20823df123a2ea8693e967b29917"}, 271 | {file = "regex-2020.11.13-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:717881211f46de3ab130b58ec0908267961fadc06e44f974466d1887f865bd5b"}, 272 | {file = "regex-2020.11.13-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:3128e30d83f2e70b0bed9b2a34e92707d0877e460b402faca908c6667092ada9"}, 273 | {file = "regex-2020.11.13-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:8f6a2229e8ad946e36815f2a03386bb8353d4bde368fdf8ca5f0cb97264d3b5c"}, 274 | {file = "regex-2020.11.13-cp39-cp39-win32.whl", hash = "sha256:f8f295db00ef5f8bae530fc39af0b40486ca6068733fb860b42115052206466f"}, 275 | {file = "regex-2020.11.13-cp39-cp39-win_amd64.whl", hash = "sha256:a15f64ae3a027b64496a71ab1f722355e570c3fac5ba2801cafce846bf5af01d"}, 276 | {file = "regex-2020.11.13.tar.gz", hash = "sha256:83d6b356e116ca119db8e7c6fc2983289d87b27b3fac238cfe5dca529d884562"}, 277 | ] 278 | toml = [ 279 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 280 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 281 | ] 282 | typed-ast = [ 283 | {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70"}, 284 | {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487"}, 285 | {file = "typed_ast-1.4.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412"}, 286 | {file = "typed_ast-1.4.2-cp35-cp35m-win32.whl", hash = "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400"}, 287 | {file = "typed_ast-1.4.2-cp35-cp35m-win_amd64.whl", hash = "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606"}, 288 | {file = "typed_ast-1.4.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64"}, 289 | {file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07"}, 290 | {file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc"}, 291 | {file = "typed_ast-1.4.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a"}, 292 | {file = "typed_ast-1.4.2-cp36-cp36m-win32.whl", hash = "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151"}, 293 | {file = "typed_ast-1.4.2-cp36-cp36m-win_amd64.whl", hash = "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3"}, 294 | {file = "typed_ast-1.4.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41"}, 295 | {file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f"}, 296 | {file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581"}, 297 | {file = "typed_ast-1.4.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37"}, 298 | {file = "typed_ast-1.4.2-cp37-cp37m-win32.whl", hash = "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd"}, 299 | {file = "typed_ast-1.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496"}, 300 | {file = "typed_ast-1.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc"}, 301 | {file = "typed_ast-1.4.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10"}, 302 | {file = "typed_ast-1.4.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea"}, 303 | {file = "typed_ast-1.4.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787"}, 304 | {file = "typed_ast-1.4.2-cp38-cp38-win32.whl", hash = "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2"}, 305 | {file = "typed_ast-1.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937"}, 306 | {file = "typed_ast-1.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1"}, 307 | {file = "typed_ast-1.4.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6"}, 308 | {file = "typed_ast-1.4.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166"}, 309 | {file = "typed_ast-1.4.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d"}, 310 | {file = "typed_ast-1.4.2-cp39-cp39-win32.whl", hash = "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b"}, 311 | {file = "typed_ast-1.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440"}, 312 | {file = "typed_ast-1.4.2.tar.gz", hash = "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a"}, 313 | ] 314 | typing-extensions = [ 315 | {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, 316 | {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, 317 | {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, 318 | ] 319 | win32-setctime = [ 320 | {file = "win32_setctime-1.0.3-py3-none-any.whl", hash = "sha256:dc925662de0a6eb987f0b01f599c01a8236cb8c62831c22d9cada09ad958243e"}, 321 | {file = "win32_setctime-1.0.3.tar.gz", hash = "sha256:4e88556c32fdf47f64165a2180ba4552f8bb32c1103a2fafd05723a0bd42bd4b"}, 322 | ] 323 | -------------------------------------------------------------------------------- /example/first-one/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "saya-example-first" 3 | version = "0.0.1" 4 | description = "" 5 | authors = ["GreyElaina <31543961+GreyElaina@users.noreply.github.com>"] 6 | license = "MIT" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.7" 10 | graia-saya = "^0.0.1" 11 | 12 | [tool.poetry.dev-dependencies] 13 | black = "^20.8b1" 14 | 15 | [build-system] 16 | requires = ["poetry-core>=1.0.0"] 17 | build-backend = "poetry.core.masonry.api" 18 | -------------------------------------------------------------------------------- /pdm.lock: -------------------------------------------------------------------------------- 1 | # This file is @generated by PDM. 2 | # It is not intended for manual editing. 3 | 4 | [metadata] 5 | groups = ["default", "broadcast", "dev", "scheduler"] 6 | strategy = ["cross_platform"] 7 | lock_version = "4.5.0" 8 | content_hash = "sha256:d3c59d1ed9dc5a704da2db1afa102bcbb03033ec82cf7bcd54e42d63e908d17c" 9 | 10 | [[metadata.targets]] 11 | requires_python = "~=3.8" 12 | 13 | [[package]] 14 | name = "black" 15 | version = "24.8.0" 16 | requires_python = ">=3.8" 17 | summary = "The uncompromising code formatter." 18 | dependencies = [ 19 | "click>=8.0.0", 20 | "mypy-extensions>=0.4.3", 21 | "packaging>=22.0", 22 | "pathspec>=0.9.0", 23 | "platformdirs>=2", 24 | "tomli>=1.1.0; python_version < \"3.11\"", 25 | "typing-extensions>=4.0.1; python_version < \"3.11\"", 26 | ] 27 | files = [ 28 | {file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"}, 29 | {file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"}, 30 | {file = "black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42"}, 31 | {file = "black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a"}, 32 | {file = "black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1"}, 33 | {file = "black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af"}, 34 | {file = "black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4"}, 35 | {file = "black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af"}, 36 | {file = "black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368"}, 37 | {file = "black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed"}, 38 | {file = "black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018"}, 39 | {file = "black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2"}, 40 | {file = "black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd"}, 41 | {file = "black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2"}, 42 | {file = "black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e"}, 43 | {file = "black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920"}, 44 | {file = "black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c"}, 45 | {file = "black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e"}, 46 | {file = "black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47"}, 47 | {file = "black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb"}, 48 | {file = "black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed"}, 49 | {file = "black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f"}, 50 | ] 51 | 52 | [[package]] 53 | name = "click" 54 | version = "8.1.6" 55 | requires_python = ">=3.7" 56 | summary = "Composable command line interface toolkit" 57 | dependencies = [ 58 | "colorama; platform_system == \"Windows\"", 59 | "importlib-metadata; python_version < \"3.8\"", 60 | ] 61 | files = [ 62 | {file = "click-8.1.6-py3-none-any.whl", hash = "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"}, 63 | {file = "click-8.1.6.tar.gz", hash = "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd"}, 64 | ] 65 | 66 | [[package]] 67 | name = "colorama" 68 | version = "0.4.6" 69 | requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 70 | summary = "Cross-platform colored terminal text." 71 | files = [ 72 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 73 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 74 | ] 75 | 76 | [[package]] 77 | name = "creart" 78 | version = "0.3.0" 79 | requires_python = ">=3.8" 80 | summary = "a universal, extensible class instantiation helper" 81 | dependencies = [ 82 | "importlib-metadata>=3.6", 83 | ] 84 | files = [ 85 | {file = "creart-0.3.0-py3-none-any.whl", hash = "sha256:43074f6f59430f41b72d3c04ba4d268af0f32842fbc94bbda4b81ae464be0ee1"}, 86 | {file = "creart-0.3.0.tar.gz", hash = "sha256:39fea77476d26d2bd5891aa3b5f16cab5567b37b855483e37f094ba005bf0d1f"}, 87 | ] 88 | 89 | [[package]] 90 | name = "croniter" 91 | version = "1.4.1" 92 | requires_python = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 93 | summary = "croniter provides iteration for datetime object with cron like format" 94 | dependencies = [ 95 | "python-dateutil", 96 | ] 97 | files = [ 98 | {file = "croniter-1.4.1-py2.py3-none-any.whl", hash = "sha256:9595da48af37ea06ec3a9f899738f1b2c1c13da3c38cea606ef7cd03ea421128"}, 99 | {file = "croniter-1.4.1.tar.gz", hash = "sha256:1a6df60eacec3b7a0aa52a8f2ef251ae3dd2a7c7c8b9874e73e791636d55a361"}, 100 | ] 101 | 102 | [[package]] 103 | name = "graia-broadcast" 104 | version = "0.23.5" 105 | requires_python = "<4.0,>=3.8" 106 | summary = "a highly customizable, elegantly designed event system based on asyncio" 107 | dependencies = [ 108 | "creart~=0.3.0", 109 | "typing-extensions>=3.10.0; python_version < \"3.9\"", 110 | ] 111 | files = [ 112 | {file = "graia_broadcast-0.23.5-py3-none-any.whl", hash = "sha256:5f294d1e929a5664d3e0d11f7d82bca5ba6939c0a132a86a72f69c24221b20db"}, 113 | {file = "graia_broadcast-0.23.5.tar.gz", hash = "sha256:4be8e7f00a177c734cec722f808d5fa9e8b157d2abe06c97f93cd7b3009af620"}, 114 | ] 115 | 116 | [[package]] 117 | name = "graia-scheduler" 118 | version = "0.3.1" 119 | requires_python = "<4.0,>=3.8" 120 | summary = "a scheduler for graia framework" 121 | dependencies = [ 122 | "creart~=0.3.0", 123 | "croniter<2.0.0,>=1.0.0", 124 | "graia-broadcast~=0.23.0", 125 | "launart>=0.7.0", 126 | ] 127 | files = [ 128 | {file = "graia_scheduler-0.3.1-py3-none-any.whl", hash = "sha256:a6783eef1a81a9815a16814eaf36c5c689065f1be2d06005f7044d68e761e71a"}, 129 | {file = "graia_scheduler-0.3.1.tar.gz", hash = "sha256:00474242858ff0d829891e1401ac155c950f6572219e0abdc384f9c62ada0bcb"}, 130 | ] 131 | 132 | [[package]] 133 | name = "importlib-metadata" 134 | version = "8.5.0" 135 | requires_python = ">=3.8" 136 | summary = "Read metadata from Python packages" 137 | dependencies = [ 138 | "typing-extensions>=3.6.4; python_version < \"3.8\"", 139 | "zipp>=3.20", 140 | ] 141 | files = [ 142 | {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, 143 | {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, 144 | ] 145 | 146 | [[package]] 147 | name = "isort" 148 | version = "5.13.2" 149 | requires_python = ">=3.8.0" 150 | summary = "A Python utility / library to sort Python imports." 151 | files = [ 152 | {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, 153 | {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, 154 | ] 155 | 156 | [[package]] 157 | name = "launart" 158 | version = "0.8.2" 159 | requires_python = ">=3.8" 160 | summary = "Component lifetime manager for runtime." 161 | dependencies = [ 162 | "creart>=0.3.0", 163 | "loguru>=0.6.0", 164 | "statv>=0.2.2", 165 | ] 166 | files = [ 167 | {file = "launart-0.8.2-py3-none-any.whl", hash = "sha256:081bcfdeb6db747cd96e479dae23c3c918ad4d570c4078a8adffcf81aeecea0c"}, 168 | {file = "launart-0.8.2.tar.gz", hash = "sha256:b1ba35d79a7cb6e3cb09195ef3b7cdf47ecaefb7838020607a0c371e188cc96c"}, 169 | ] 170 | 171 | [[package]] 172 | name = "loguru" 173 | version = "0.7.2" 174 | requires_python = ">=3.5" 175 | summary = "Python logging made (stupidly) simple" 176 | dependencies = [ 177 | "aiocontextvars>=0.2.0; python_version < \"3.7\"", 178 | "colorama>=0.3.4; sys_platform == \"win32\"", 179 | "win32-setctime>=1.0.0; sys_platform == \"win32\"", 180 | ] 181 | files = [ 182 | {file = "loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb"}, 183 | {file = "loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac"}, 184 | ] 185 | 186 | [[package]] 187 | name = "mypy-extensions" 188 | version = "1.0.0" 189 | requires_python = ">=3.5" 190 | summary = "Type system extensions for programs checked with the mypy type checker." 191 | files = [ 192 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 193 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 194 | ] 195 | 196 | [[package]] 197 | name = "packaging" 198 | version = "23.1" 199 | requires_python = ">=3.7" 200 | summary = "Core utilities for Python packages" 201 | files = [ 202 | {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, 203 | {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, 204 | ] 205 | 206 | [[package]] 207 | name = "pathspec" 208 | version = "0.11.2" 209 | requires_python = ">=3.7" 210 | summary = "Utility library for gitignore style pattern matching of file paths." 211 | files = [ 212 | {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, 213 | {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, 214 | ] 215 | 216 | [[package]] 217 | name = "platformdirs" 218 | version = "3.10.0" 219 | requires_python = ">=3.7" 220 | summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 221 | dependencies = [ 222 | "typing-extensions>=4.7.1; python_version < \"3.8\"", 223 | ] 224 | files = [ 225 | {file = "platformdirs-3.10.0-py3-none-any.whl", hash = "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"}, 226 | {file = "platformdirs-3.10.0.tar.gz", hash = "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d"}, 227 | ] 228 | 229 | [[package]] 230 | name = "python-dateutil" 231 | version = "2.8.2" 232 | requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 233 | summary = "Extensions to the standard Python datetime module" 234 | dependencies = [ 235 | "six>=1.5", 236 | ] 237 | files = [ 238 | {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, 239 | {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, 240 | ] 241 | 242 | [[package]] 243 | name = "six" 244 | version = "1.16.0" 245 | requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 246 | summary = "Python 2 and 3 compatibility utilities" 247 | files = [ 248 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 249 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 250 | ] 251 | 252 | [[package]] 253 | name = "statv" 254 | version = "0.3.2" 255 | requires_python = ">=3.8" 256 | summary = "a uniform status implementation for graia project" 257 | files = [ 258 | {file = "statv-0.3.2-py3-none-any.whl", hash = "sha256:32e430b21ab6a62695c67ab6cae3dba6e01e9c3fdbc9344e3236ad7a1f51d0c7"}, 259 | {file = "statv-0.3.2.tar.gz", hash = "sha256:fb4df2a37bf7a792e36a6e657c74cbdef9e2cdb8de3a0762b28b35c9e13f0fdd"}, 260 | ] 261 | 262 | [[package]] 263 | name = "tomli" 264 | version = "2.0.1" 265 | requires_python = ">=3.7" 266 | summary = "A lil' TOML parser" 267 | files = [ 268 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 269 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 270 | ] 271 | 272 | [[package]] 273 | name = "typing-extensions" 274 | version = "4.12.2" 275 | requires_python = ">=3.8" 276 | summary = "Backported and Experimental Type Hints for Python 3.8+" 277 | files = [ 278 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 279 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 280 | ] 281 | 282 | [[package]] 283 | name = "win32-setctime" 284 | version = "1.1.0" 285 | requires_python = ">=3.5" 286 | summary = "A small Python utility to set file creation time on Windows" 287 | files = [ 288 | {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, 289 | {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, 290 | ] 291 | 292 | [[package]] 293 | name = "zipp" 294 | version = "3.20.2" 295 | requires_python = ">=3.8" 296 | summary = "Backport of pathlib-compatible object wrapper for zip files" 297 | files = [ 298 | {file = "zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350"}, 299 | {file = "zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29"}, 300 | ] 301 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.pdm] 2 | [tool.pdm.dev-dependencies] 3 | dev = [ 4 | "black", 5 | "isort", 6 | "creart>=0.3.0", 7 | ] 8 | 9 | [tool.pdm.build] 10 | includes = [ 11 | "src/graia", 12 | ] 13 | 14 | [build-system] 15 | requires = ["pdm-backend"] 16 | build-backend = "pdm.backend" 17 | 18 | [project] 19 | authors = [ 20 | {name = "GreyElaina", email = "31543961+GreyElaina@users.noreply.github.com"}, 21 | ] 22 | requires-python = ">=3.8,<4.0" 23 | dependencies = [ 24 | "importlib-metadata>=3.6.0", 25 | "loguru<1.0,>=0.6.0", 26 | "typing-extensions>=4.5.0", 27 | ] 28 | name = "graia-saya" 29 | version = "0.0.20" 30 | description = "a modular implementation with modern design and injection" 31 | license = {text = "MIT"} 32 | readme = "README.md" 33 | 34 | [project.optional-dependencies] 35 | broadcast = [ 36 | "graia-broadcast>=0.23.0", 37 | ] 38 | scheduler = [ 39 | "graia-scheduler<1.0.0,>=0.2.0", 40 | ] 41 | 42 | [project.entry-points."creart.creators"] 43 | saya = "graia.saya.creator:SayaCreator" 44 | broadcast_behaviour = "graia.saya.creator:BroadcastBehaviourCreator" 45 | -------------------------------------------------------------------------------- /src/graia/saya/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import importlib 4 | import sys 5 | from contextlib import contextmanager 6 | from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union 7 | 8 | from loguru import logger 9 | 10 | from graia.saya.behaviour import Behaviour, BehaviourInterface 11 | from graia.saya.channel import Channel 12 | 13 | from typing_extensions import deprecated 14 | 15 | from .context import channel_instance, environment_metadata, saya_instance 16 | 17 | if TYPE_CHECKING: 18 | from graia.broadcast import Broadcast 19 | 20 | 21 | class Saya: 22 | """Modular application for Graia Framework. 23 | 24 | > 名称取自作品 魔女之旅 中的角色 "沙耶(Saya)", 愿所有人的心中都有一位活泼可爱的炭之魔女. 25 | 26 | Saya 的架构分为: `Saya Controller`(控制器), `Module Channel`(模块容器), `Cube`(内容容器), `Behaviour`(行为). 27 | 28 | - `Saya Controller` 负责管理各个模块的引入, 即本 `Saya` 类 29 | - `Module Channel` 负责在模块内收集组件信息 30 | - `Cube` 则用于使用 `Schema` 构造的元信息实例去描述内容 31 | - `Behaviour` 用于通过对 `Cube` 内的元信息进行解析, 并提供对其他已有接口的 Native 支持 32 | """ 33 | 34 | behaviour_interface: BehaviourInterface 35 | behaviours: List[Behaviour] 36 | channels: Dict[str, Channel] 37 | broadcast: Optional[Broadcast] 38 | 39 | mounts: Dict[str, Any] 40 | 41 | def __init__(self, broadcast: Optional[Broadcast] = None) -> None: 42 | self.channels = {} 43 | self.behaviours = [] 44 | self.behaviour_interface = BehaviourInterface(self) 45 | self.behaviour_interface.require_contents[0].behaviours = self.behaviours 46 | 47 | self.mounts = {} 48 | self.broadcast = broadcast 49 | 50 | @contextmanager 51 | def module_context(self): 52 | saya_token = saya_instance.set(self) 53 | yield 54 | saya_instance.reset(saya_token) 55 | 56 | @staticmethod 57 | def current() -> "Saya": 58 | """返回当前上下文中的 Saya 实例 59 | 60 | Returns: 61 | Saya: 当前上下文中的 Saya 实例 62 | """ 63 | return saya_instance.get() 64 | 65 | def require_resolve(self, module: str) -> Channel: 66 | """导入模块, 并注入 Channel, 模块完成内容注册后返回该 Channel 67 | 68 | Args: 69 | module (str): 需为可被当前运行时访问的 Python Module 的引入路径 70 | 71 | Returns: 72 | Channel: 已注册了内容的 Channel. 73 | """ 74 | channel = Channel(module) 75 | channel_token = channel_instance.set(channel) 76 | 77 | try: 78 | imported_module = importlib.import_module(module, module) 79 | channel._py_module = imported_module 80 | with self.behaviour_interface.require_context(module) as interface: 81 | for cube in channel.content: 82 | try: 83 | interface.allocate_cube(cube) 84 | except: 85 | logger.exception(f"an error occurred while loading the module's cube: {module}::{cube}") 86 | raise 87 | finally: 88 | channel_instance.reset(channel_token) 89 | 90 | return channel 91 | 92 | @staticmethod 93 | def current_env() -> Any: 94 | """只能用于模块内. 返回在调用 Saya.require 方法时传入的 `require_env` 参数的值 95 | 96 | Returns: 97 | Any: 在调用 Saya.require 方法时传入的 `require_env` 参数的值 98 | """ 99 | return environment_metadata.get(None) 100 | 101 | def require(self, module: str, require_env: Any = None) -> Union[Channel, Any]: 102 | """既处理 Channel, 也处理 Channel 中的 export 返回 103 | 104 | Args: 105 | module (str): 需为可被当前运行时访问的 Python Module 的引入路径 106 | require_env (Any, optional): 可以把这个参数作为硬编码配置使用, 虽然应该是叫传入 "环境条件". 107 | 108 | Returns: 109 | Union[Channel, Any]: 大多数情况下应该是 Channel, 如果设定了 export 就不一样了 110 | """ 111 | logger.debug(f"require {module}") 112 | 113 | if module in self.channels: 114 | channel = self.channels[module] 115 | if channel._export: 116 | return channel._export 117 | return channel 118 | 119 | env_token = environment_metadata.set(require_env) 120 | channel = self.require_resolve(module) 121 | self.channels[module] = channel 122 | environment_metadata.reset(env_token) 123 | 124 | if self.broadcast: 125 | from .event import SayaModuleInstalled 126 | 127 | token = saya_instance.set(self) 128 | self.broadcast.postEvent( 129 | SayaModuleInstalled( 130 | module=module, 131 | channel=channel, 132 | ) 133 | ) 134 | saya_instance.reset(token) 135 | 136 | logger.info(f"module loading finished: {module}") 137 | 138 | if channel._export: 139 | return channel._export 140 | 141 | return channel 142 | 143 | def install_behaviours(self, *behaviours: Behaviour): 144 | """在控制器中注册 Behaviour, 用于处理模块提供的内容""" 145 | self.behaviours.extend(behaviours) 146 | 147 | def uninstall_channel(self, channel: Channel): 148 | """卸载指定的 Channel 149 | 150 | Args: 151 | channel (Channel): 需要卸载的 Channel 152 | 153 | Raises: 154 | TypeError: 提供的 Channel 不在本 Saya 实例内 155 | ValueError: 尝试卸载 __main__, 即主程序所属的模块 156 | """ 157 | if channel not in self.channels.values(): 158 | raise TypeError("assert an existed channel") 159 | 160 | if channel.module == "__main__": 161 | raise ValueError("main channel cannot uninstall") 162 | 163 | # TODO: builtin signal(async or sync) 164 | if self.broadcast: 165 | from .event import SayaModuleUninstall 166 | 167 | token = saya_instance.set(self) 168 | self.broadcast.postEvent( 169 | SayaModuleUninstall( 170 | module=channel.module, 171 | channel=channel, 172 | ) 173 | ) 174 | saya_instance.reset(token) 175 | 176 | with self.behaviour_interface.require_context(channel.module) as interface: 177 | for cube in channel.content: 178 | try: 179 | interface.release_cube(cube) 180 | except: 181 | logger.exception(f"an error occurred while loading the module's cube: {channel.module}::{cube}") 182 | raise 183 | 184 | del self.channels[channel.module] 185 | channel._py_module = None 186 | 187 | if sys.modules.get(channel.module): 188 | del sys.modules[channel.module] 189 | 190 | if self.broadcast: 191 | from .event import SayaModuleUninstalled 192 | 193 | token = saya_instance.set(self) 194 | self.broadcast.postEvent( 195 | SayaModuleUninstalled( 196 | module=channel.module, 197 | ) 198 | ) 199 | saya_instance.reset(token) 200 | 201 | def reload_channel(self, channel: Channel) -> None: 202 | """重载指定的模块 203 | 204 | Args: 205 | channel (Channel): 指定需要重载的模块, 请使用 channels.get 方法获取 206 | 207 | Raises: 208 | TypeError: 没有给定需要被重载的模块(`channel` 与 `module` 都不给定) 209 | ValueError: 没有通过 `module` 找到对应的 Channel 210 | """ 211 | self.uninstall_channel(channel) 212 | new_channel: Channel = self.require_resolve(channel.module) 213 | 214 | channel.meta = new_channel.meta 215 | channel._export = new_channel._export 216 | channel._py_module = new_channel._py_module 217 | channel.content = new_channel.content 218 | 219 | self.channels[channel.module] = channel 220 | 221 | @deprecated("create_main_channel is deprecated, use main_context instead", category=DeprecationWarning) 222 | def create_main_channel(self) -> Channel: 223 | """创建不可被卸载的 `__main__` 主程序模块 224 | 225 | Returns: 226 | Channel: 属性 `name` 值为 `__main__`, 且无法被 `uninstall_channel` 卸载的模块. 227 | """ 228 | may_current = self.channels.get("__main__") 229 | if may_current: 230 | return may_current 231 | 232 | main_channel = Channel("__main__") 233 | self.channels["__main__"] = main_channel 234 | 235 | if self.broadcast: 236 | from .event import SayaModuleInstalled 237 | 238 | token = saya_instance.set(self) 239 | self.broadcast.postEvent( 240 | SayaModuleInstalled( 241 | module="__main__", 242 | channel=main_channel, 243 | ) 244 | ) 245 | saya_instance.reset(token) 246 | 247 | return main_channel 248 | 249 | @contextmanager 250 | def main_context(self): 251 | """创建或加载不可被卸载的 `__main__` 主程序上下文, 如同使用 `Saya.require` 加载模块一样 252 | 253 | 在单文件结构/临时测试环境下推荐使用该方法; 在正式项目中, 请使用 `Saya.require` 加载模块. 254 | 255 | Examples: 256 | ```python 257 | >>> with saya.main_context() as main_channel: 258 | >>> # do something 259 | ``` 260 | """ 261 | if "__main__" not in self.channels: 262 | main_channel = Channel("__main__") 263 | self.channels["__main__"] = main_channel 264 | else: 265 | main_channel = self.channels["__main__"] 266 | token = channel_instance.set(main_channel) 267 | yield main_channel 268 | try: 269 | with self.behaviour_interface.require_context("__main__") as interface: 270 | for cube in main_channel.content: 271 | try: 272 | interface.allocate_cube(cube) 273 | except: 274 | logger.exception(f"an error occurred while loading the module's cube: __main__::{cube}") 275 | raise 276 | finally: 277 | channel_instance.reset(token) 278 | 279 | if self.broadcast: 280 | from .event import SayaModuleInstalled 281 | 282 | token = saya_instance.set(self) 283 | self.broadcast.postEvent( 284 | SayaModuleInstalled( 285 | module="__main__", 286 | channel=main_channel, 287 | ) 288 | ) 289 | saya_instance.reset(token) 290 | 291 | def mount(self, mount_point: str, target): 292 | """挂载实例到 Saya 下, 以便整个模块系统共用. 293 | 294 | Args: 295 | mount_point (str): 指定的挂载点, 建议使用类似 `saya.builtin.asyncio.event_loop` 这样的形式 296 | target (Any): 需要挂载的实例 297 | 298 | Returns: 299 | NoReturn: 已将实例挂载, 可能把已经注册的挂载点给覆盖了. 300 | """ 301 | self.mounts[mount_point] = target 302 | 303 | def unmount(self, mount_point: str): 304 | """删除挂载及其挂载点 305 | 306 | Args: 307 | mount_point (str): 目标挂载点 308 | 309 | Returns: 310 | NoReturn: 已经删除 311 | 312 | Raises: 313 | KeyError: 挂载点不存在 314 | """ 315 | del self.mounts[mount_point] 316 | 317 | def access(self, mount_point: str): 318 | """访问特定挂载点 319 | 320 | Args: 321 | mount_point (str): 目标挂载点 322 | 323 | Returns: 324 | Any: 已经挂载的实例 325 | 326 | Raises: 327 | KeyError: 挂载点不存在 328 | """ 329 | return self.mounts[mount_point] 330 | -------------------------------------------------------------------------------- /src/graia/saya/behaviour/__init__.py: -------------------------------------------------------------------------------- 1 | from .context import AllocationContext as AllocationContext 2 | from .context import RequireContext as RequireContext 3 | from .entity import Behaviour as Behaviour 4 | from .interface import BehaviourInterface as BehaviourInterface 5 | -------------------------------------------------------------------------------- /src/graia/saya/behaviour/context.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import TYPE_CHECKING, List 3 | 4 | if TYPE_CHECKING: 5 | from graia.saya.cube import Cube 6 | 7 | from .entity import Behaviour 8 | 9 | 10 | @dataclass(init=True) 11 | class RequireContext: 12 | module: str 13 | behaviours: List["Behaviour"] 14 | _index: int = 0 15 | 16 | 17 | @dataclass(init=True) 18 | class AllocationContext: 19 | cube: "Cube" 20 | 21 | 22 | @dataclass(init=True) 23 | class RouteContext: 24 | module: str 25 | -------------------------------------------------------------------------------- /src/graia/saya/behaviour/entity.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | from typing import Any 3 | 4 | from graia.saya.cube import Cube 5 | 6 | 7 | class Behaviour(metaclass=ABCMeta): 8 | @abstractmethod 9 | def allocate(self, cube: Cube[Any]) -> Any: 10 | pass 11 | 12 | @abstractmethod 13 | def release(self, cube: Cube[Any]) -> Any: 14 | pass 15 | -------------------------------------------------------------------------------- /src/graia/saya/behaviour/interface.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | from typing import TYPE_CHECKING, Any, Generator, List, Optional 3 | 4 | from graia.broadcast.exceptions import RequirementCrashed 5 | 6 | from graia.saya.cube import Cube 7 | 8 | from .context import AllocationContext, RequireContext 9 | from .entity import Behaviour 10 | 11 | if TYPE_CHECKING: 12 | from graia.saya import Saya 13 | 14 | # TODO: Lifecycle for Behaviour 15 | 16 | 17 | class BehaviourInterface: 18 | saya: "Saya" 19 | 20 | require_contents: List[RequireContext] 21 | 22 | def __init__(self, saya_instance: "Saya") -> None: 23 | self.saya = saya_instance 24 | self.require_contents = [RequireContext("graia.saya.__special__.global_behaviours", [])] 25 | 26 | @property 27 | def currentModule(self): 28 | return self.require_contents[-1].module 29 | 30 | @property 31 | def _index(self): 32 | return self.require_contents[-1]._index 33 | 34 | def require_context(self, module: str, behaviours: Optional[List["Behaviour"]] = None): 35 | self.require_contents.append(RequireContext(module, behaviours or [])) 36 | return self 37 | 38 | def __enter__(self) -> "BehaviourInterface": 39 | return self 40 | 41 | def __exit__(self, _, exc: Exception, tb): 42 | self.require_contents.pop() # just simple. 43 | if tb is not None: 44 | raise exc.with_traceback(tb) 45 | 46 | def behaviour_generator(self): 47 | yield from self.require_contents[0].behaviours 48 | # Cube 没有 behaviours 设定, 哦, 连 always 都没有. 49 | yield from self.require_contents[-1].behaviours 50 | 51 | def allocate_cube(self, cube: Cube) -> Any: 52 | start_offset = self._index + int(bool(self._index)) 53 | 54 | for self.require_contents[-1]._index, behaviour in enumerate( 55 | itertools.islice(self.behaviour_generator(), start_offset, None, None), 56 | start=start_offset, 57 | ): 58 | result = behaviour.allocate(cube) 59 | 60 | if result is None: 61 | continue 62 | 63 | self.require_contents[-1]._index = 0 64 | return result 65 | else: 66 | raise RequirementCrashed(f"the dispatching requirement crashed: {cube}") 67 | 68 | def release_cube(self, cube: Cube) -> Any: 69 | start_offset = self._index + int(bool(self._index)) 70 | 71 | for self.require_contents[-1]._index, behaviour in enumerate( 72 | itertools.islice(self.behaviour_generator(), start_offset, None, None), 73 | start=start_offset, 74 | ): 75 | result = behaviour.release(cube) 76 | 77 | if result is None: 78 | continue 79 | 80 | self.require_contents[-1]._index = 0 81 | return result 82 | else: 83 | raise RequirementCrashed(f"the dispatching requirement crashed: {cube}") 84 | -------------------------------------------------------------------------------- /src/graia/saya/builtins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GraiaProject/Saya/c7ac639cb0e4c3b95c6db07603b5973d74081f5c/src/graia/saya/builtins/__init__.py -------------------------------------------------------------------------------- /src/graia/saya/builtins/broadcast/__init__.py: -------------------------------------------------------------------------------- 1 | from .behaviour import BroadcastBehaviour 2 | from .schema import ListenerSchema 3 | 4 | __all__ = ("BroadcastBehaviour", "ListenerSchema") 5 | -------------------------------------------------------------------------------- /src/graia/saya/builtins/broadcast/behaviour.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from graia.broadcast import Broadcast 4 | 5 | from graia.saya.behaviour import Behaviour 6 | from graia.saya.cube import Cube 7 | 8 | from .schema import ListenerSchema 9 | 10 | 11 | class BroadcastBehaviour(Behaviour): 12 | broadcast: Broadcast 13 | 14 | def __init__(self, broadcast: Broadcast) -> None: 15 | self.broadcast = broadcast 16 | 17 | def allocate( 18 | self, 19 | cube: Cube[ListenerSchema], 20 | ): 21 | if isinstance(cube.metaclass, ListenerSchema): 22 | listener = cube.metaclass.build_listener(cube.content, self.broadcast) 23 | if not listener.namespace: 24 | listener.namespace = self.broadcast.getDefaultNamespace() 25 | self.broadcast.listeners.append(listener) 26 | else: 27 | return 28 | return True 29 | 30 | def release(self, cube: Cube) -> Any: 31 | if isinstance(cube.metaclass, ListenerSchema): 32 | self.broadcast.removeListener(self.broadcast.getListener(cube.content)) 33 | else: 34 | return 35 | 36 | return True 37 | -------------------------------------------------------------------------------- /src/graia/saya/builtins/broadcast/schema.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from typing import Callable, Dict, List, Optional, Type 3 | 4 | from graia.broadcast import Broadcast 5 | from graia.broadcast.entities.decorator import Decorator 6 | from graia.broadcast.entities.event import Dispatchable 7 | from graia.broadcast.entities.listener import Listener 8 | from graia.broadcast.entities.namespace import Namespace 9 | from graia.broadcast.typing import T_Dispatcher 10 | from graia.saya.schema import BaseSchema 11 | 12 | 13 | @dataclass 14 | class ListenerSchema(BaseSchema): 15 | listening_events: List[Type[Dispatchable]] 16 | namespace: Optional[Namespace] = None 17 | inline_dispatchers: List[T_Dispatcher] = field(default_factory=list) 18 | decorators: List[Decorator] = field(default_factory=list) 19 | priority: int = 16 20 | extra_priorities: Dict[Type[Dispatchable], int] = field(default_factory=dict) 21 | 22 | def build_listener(self, callable: Callable, broadcast: "Broadcast"): 23 | listener = Listener( 24 | callable=callable, 25 | namespace=self.namespace or broadcast.getDefaultNamespace(), 26 | listening_events=self.listening_events, 27 | inline_dispatchers=self.inline_dispatchers, 28 | decorators=self.decorators, 29 | priority=self.priority, 30 | ) 31 | if hasattr(listener, "priorities"): # backward compatibility 32 | listener.priorities.update(self.extra_priorities.items()) # for type checkers 33 | return listener 34 | -------------------------------------------------------------------------------- /src/graia/saya/builtins/broadcast/shortcut.py: -------------------------------------------------------------------------------- 1 | """Saya 相关的工具""" 2 | from __future__ import annotations 3 | 4 | import inspect 5 | from typing import ( 6 | Any, 7 | Callable, 8 | Dict, 9 | Generator, 10 | List, 11 | Type, 12 | TypeVar, 13 | Union, 14 | overload, 15 | ) 16 | 17 | from graia.broadcast.entities.decorator import Decorator 18 | from graia.broadcast.entities.event import Dispatchable 19 | from graia.broadcast.typing import T_Dispatcher 20 | from graia.saya.factory import BufferModifier, SchemaWrapper, buffer_modifier, factory 21 | 22 | from .schema import ListenerSchema 23 | 24 | T_Callable = TypeVar("T_Callable", bound=Callable) 25 | Wrapper = Callable[[T_Callable], T_Callable] 26 | 27 | T = TypeVar("T") 28 | 29 | 30 | def gen_subclass(cls: type[T]) -> Generator[type[T], Any, Any]: 31 | yield cls 32 | for sub in cls.__subclasses__(): 33 | yield from gen_subclass(sub) 34 | 35 | 36 | @buffer_modifier 37 | def dispatch(*dispatcher: T_Dispatcher) -> BufferModifier: 38 | """附加参数解析器,最后必须接 `listen` 才能起效 39 | 40 | Args: 41 | *dispatcher (T_Dispatcher): 参数解析器 42 | 43 | Returns: 44 | Callable[[T_Callable], T_Callable]: 装饰器 45 | """ 46 | 47 | return lambda buffer: buffer.setdefault("dispatchers", []).extend(dispatcher) 48 | 49 | 50 | @overload 51 | def decorate(*decorator: Decorator) -> Wrapper: 52 | """附加多个无头装饰器 53 | 54 | Args: 55 | *decorator (Decorator): 无头装饰器 56 | 57 | Returns: 58 | Callable[[T_Callable], T_Callable]: 装饰器 59 | """ 60 | ... 61 | 62 | 63 | @overload 64 | def decorate(name: str, decorator: Decorator, /) -> Wrapper: 65 | """给指定参数名称附加装饰器 66 | 67 | Args: 68 | name (str): 参数名称 69 | decorator (Decorator): 装饰器 70 | 71 | Returns: 72 | Callable[[T_Callable], T_Callable]: 装饰器 73 | """ 74 | ... 75 | 76 | 77 | @overload 78 | def decorate(mapping: Dict[str, Decorator], /) -> Wrapper: 79 | """给指定参数名称附加装饰器 80 | 81 | Args: 82 | mapping (Dict[str, Decorator]): 参数名称与装饰器的映射 83 | 84 | Returns: 85 | Callable[[T_Callable], T_Callable]: 装饰器 86 | """ 87 | ... 88 | 89 | 90 | @buffer_modifier 91 | def decorate(*args) -> BufferModifier: 92 | """给指定参数名称附加装饰器 93 | 94 | Args: 95 | name (str | Dict[str, Decorator]): 参数名称或与装饰器的映射 96 | decorator (Decorator): 装饰器 97 | 98 | Returns: 99 | Callable[[T_Callable], T_Callable]: 装饰器 100 | """ 101 | arg: Union[Dict[str, Decorator], List[Decorator]] 102 | if isinstance(args[0], str): 103 | name: str = args[0] 104 | decorator: Decorator = args[1] 105 | arg = {name: decorator} 106 | elif isinstance(args[0], dict): 107 | arg = args[0] 108 | else: 109 | arg = list(args) 110 | 111 | def wrapper(buffer: Dict[str, Any]) -> None: 112 | if isinstance(arg, list): 113 | buffer.setdefault("decorators", []).extend(arg) 114 | elif isinstance(arg, dict): 115 | buffer.setdefault("decorator_map", {}).update(arg) 116 | 117 | return wrapper 118 | 119 | 120 | @buffer_modifier 121 | def priority(level: int, *events: Type[Dispatchable]) -> BufferModifier: 122 | """设置事件优先级 123 | 124 | Args: 125 | level (int): 事件优先级 126 | *events (Type[Dispatchable]): 提供时则会设置这些事件的优先级, 否则设置全局优先级 127 | 128 | Returns: 129 | Callable[[T_Callable], T_Callable]: 装饰器 130 | """ 131 | 132 | def wrapper(buffer: Dict[str, Any]) -> None: 133 | if events: 134 | buffer.setdefault("extra_priorities", {}).update((e, level) for e in events) 135 | else: 136 | buffer["priority"] = level 137 | 138 | return wrapper 139 | 140 | 141 | @factory 142 | def listen(*event: Union[Type[Dispatchable], str]) -> SchemaWrapper: 143 | """在当前 Saya Channel 中监听指定事件 144 | 145 | Args: 146 | *event (Union[Type[Dispatchable], str]): 事件类型或事件名称 147 | 148 | Returns: 149 | Callable[[T_Callable], T_Callable]: 装饰器 150 | """ 151 | EVENTS: Dict[str, Type[Dispatchable]] = {e.__name__: e for e in gen_subclass(Dispatchable)} 152 | events: List[Type[Dispatchable]] = [e if isinstance(e, type) else EVENTS[e] for e in event] 153 | 154 | def wrapper(func: Callable, buffer: Dict[str, Any]) -> ListenerSchema: 155 | decorator_map: Dict[str, Decorator] = buffer.pop("decorator_map", {}) 156 | buffer["inline_dispatchers"] = buffer.pop("dispatchers", []) 157 | if decorator_map: 158 | sig = inspect.signature(func) 159 | for param in sig.parameters.values(): 160 | if decorator := decorator_map.get(param.name): 161 | setattr(param, "_default", decorator) 162 | func.__signature__ = sig 163 | return ListenerSchema(listening_events=events, **buffer) 164 | 165 | return wrapper 166 | -------------------------------------------------------------------------------- /src/graia/saya/channel.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import functools 4 | import inspect 5 | from types import ModuleType 6 | from typing import ( 7 | Any, 8 | Callable, 9 | Dict, 10 | Generic, 11 | List, 12 | Optional, 13 | Type, 14 | TypedDict, 15 | TypeVar, 16 | Union, 17 | cast, 18 | ) 19 | 20 | from importlib_metadata import distribution 21 | from typing_extensions import NotRequired 22 | 23 | from .cube import Cube 24 | from .context import channel_instance 25 | from .schema import BaseSchema 26 | 27 | 28 | class ChannelMeta(TypedDict): 29 | author: List[str] 30 | name: NotRequired[str] 31 | version: NotRequired[str] 32 | license: NotRequired[str] 33 | urls: NotRequired[Dict[str, str]] 34 | description: NotRequired[str] 35 | icon: NotRequired[str] 36 | classifier: List[str] 37 | dependencies: List[str] 38 | 39 | standards: List[str] 40 | frameworks: List[str] 41 | config_endpoints: List[str] 42 | component_endpoints: List[str] 43 | 44 | 45 | def _default_channel_meta() -> ChannelMeta: 46 | return ChannelMeta( 47 | author=[], 48 | classifier=[], 49 | dependencies=[], 50 | standards=[], 51 | frameworks=[], 52 | config_endpoints=[], 53 | component_endpoints=[], 54 | ) 55 | 56 | 57 | def get_channel_meta(module: str) -> ChannelMeta: 58 | dist = distribution(module) 59 | meta = cast(Dict[str, Any], _default_channel_meta()) 60 | meta |= dist.metadata.json 61 | if meta["author"]: # "author" in dist.metadata.json and dist.metadata.json["author"] 62 | meta["author"] = meta["author"].split(",") 63 | elif "author_email" in meta: 64 | meta["author"] = meta["author_email"].split(",") 65 | meta["urls"] = dict(i.split(", ") for i in meta.get("project_url", ())) 66 | meta["dependencies"] = dist.requires or [] 67 | return cast(ChannelMeta, meta) 68 | 69 | 70 | M = TypeVar("M", bound=ChannelMeta) 71 | 72 | 73 | class ScopedContext: 74 | content: Dict[str, Any] 75 | 76 | def __init__(self, **kwargs) -> None: 77 | object.__setattr__(self, "content", kwargs) 78 | 79 | def __setattr__(self, __name: str, __value: Any) -> None: 80 | if __name == "content": 81 | raise AttributeError("'ScopedContext' object attribute 'content' is not overwritable") 82 | self.content[__name] = __value 83 | 84 | def __getattr__(self, __name: str) -> Any: 85 | if __name == "content": 86 | return self.content 87 | if __name not in self.content: 88 | raise AttributeError(f"{__name} is not defined") 89 | return self.content[__name] 90 | 91 | 92 | class Channel(Generic[M]): 93 | module: str 94 | 95 | meta: M 96 | 97 | _export: Any = None 98 | _py_module: Optional[ModuleType] = None 99 | 100 | content: List[Cube] 101 | 102 | scopes: Dict[Type, ScopedContext] 103 | 104 | # TODO: _export reload for other modules 105 | 106 | def __init__(self, module: str) -> None: 107 | self.module = module 108 | self.meta = cast(M, _default_channel_meta()) 109 | self.content = [] 110 | self.scopes = {} 111 | 112 | @staticmethod 113 | def current() -> "Channel": 114 | """获取当前的 Channel 对象 115 | 116 | Returns: 117 | Channel: 当前的 Channel 对象 118 | """ 119 | return channel_instance.get() 120 | 121 | def export(self, target): 122 | self._export = target 123 | return target 124 | 125 | def use(self, schema: BaseSchema): 126 | def use_wrapper(target: Union[Type, Callable, Any]): 127 | self.content.append(Cube(target, schema)) 128 | return target 129 | 130 | return use_wrapper 131 | 132 | def cancel(self, target: Union[Type, Callable, Any]): 133 | self.content = [i for i in self.content if i.content is not target] 134 | 135 | def scoped_context(self, isolate_class: Type[Any]): 136 | context = self.scopes.setdefault( 137 | isolate_class, 138 | ScopedContext(channel=self), 139 | ) 140 | contents = {id(i.content): i for i in self.content} 141 | members = inspect.getmembers(isolate_class, lambda obj: callable(obj) and id(obj) in contents) 142 | # 用 id 的原因: 防止一些 unhashable 的对象给我塞进来. 143 | for _, obj in members: 144 | contents[id(obj)].content = functools.partial(obj, context) 145 | return isolate_class 146 | -------------------------------------------------------------------------------- /src/graia/saya/context.py: -------------------------------------------------------------------------------- 1 | from contextvars import ContextVar 2 | 3 | saya_instance = ContextVar("saya_instance") 4 | channel_instance = ContextVar("channel") 5 | 6 | environment_metadata = ContextVar("environment_metadata") 7 | -------------------------------------------------------------------------------- /src/graia/saya/creator.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from contextlib import suppress 4 | from typing import TYPE_CHECKING 5 | 6 | from creart import AbstractCreator, CreateTargetInfo, exists_module, it 7 | 8 | from . import Saya 9 | 10 | if TYPE_CHECKING: 11 | from .builtins.broadcast import BroadcastBehaviour 12 | 13 | 14 | class SayaCreator(AbstractCreator): 15 | targets = ( 16 | CreateTargetInfo( 17 | module="graia.saya", 18 | identify="Saya", 19 | humanized_name="Saya", 20 | description=" a modular implementation with modern design and injection", 21 | author=["GraiaProject@github"], 22 | ), 23 | ) 24 | 25 | @staticmethod 26 | def create(create_type: type[Saya]) -> Saya: 27 | try: 28 | from graia.broadcast import Broadcast 29 | 30 | from .builtins.broadcast import BroadcastBehaviour 31 | 32 | broadcast = it(Broadcast) 33 | saya = create_type(broadcast) 34 | saya.install_behaviours(it(BroadcastBehaviour)) 35 | except (ImportError, TypeError): 36 | saya = create_type() 37 | 38 | with suppress(ImportError, TypeError): 39 | from graia.scheduler.saya.behaviour import GraiaSchedulerBehaviour 40 | 41 | saya.install_behaviours(it(GraiaSchedulerBehaviour)) 42 | 43 | return saya 44 | 45 | 46 | class BroadcastBehaviourCreator(AbstractCreator): 47 | targets = ( 48 | CreateTargetInfo( 49 | "graia.saya.builtins.broadcast.behaviour", "BroadcastBehaviour" 50 | ), 51 | ) 52 | 53 | @staticmethod 54 | def available() -> bool: 55 | return exists_module("graia.broadcast") 56 | 57 | @staticmethod 58 | def create(create_type: type[BroadcastBehaviour]) -> BroadcastBehaviour: 59 | from graia.broadcast import Broadcast 60 | 61 | broadcast = it(Broadcast) 62 | return create_type(broadcast) 63 | -------------------------------------------------------------------------------- /src/graia/saya/cube.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Any, Generic, Optional, TypeVar 3 | 4 | from .schema import BaseSchema 5 | 6 | T = TypeVar("T", bound=Optional[BaseSchema]) 7 | 8 | 9 | @dataclass(init=True) 10 | class Cube(Generic[T]): 11 | content: Any 12 | metaclass: T 13 | -------------------------------------------------------------------------------- /src/graia/saya/event.py: -------------------------------------------------------------------------------- 1 | from graia.broadcast.entities.dispatcher import BaseDispatcher 2 | from graia.broadcast.entities.event import Dispatchable 3 | from graia.broadcast.interfaces.dispatcher import DispatcherInterface 4 | 5 | from graia.saya.channel import Channel 6 | from graia.saya.context import saya_instance 7 | 8 | 9 | class SayaModuleInstalled(Dispatchable): 10 | """不用返回 RemoveMe, 因为 Cube 在被 uninstall 时会被清理掉, 如果你用的是规范的 Saya Protocol 的话.""" 11 | 12 | module: str 13 | channel: Channel 14 | 15 | def __init__(self, module: str, channel: Channel) -> None: 16 | self.module = module 17 | self.channel = channel 18 | 19 | class Dispatcher(BaseDispatcher): 20 | @staticmethod 21 | async def catch(interface: "DispatcherInterface[SayaModuleInstalled]"): 22 | from graia.saya import Saya 23 | 24 | if interface.annotation is Saya: 25 | return saya_instance.get() 26 | elif interface.annotation is Channel: 27 | return interface.event.channel 28 | 29 | 30 | class SayaModuleUninstall(Dispatchable): 31 | """不用返回 RemoveMe, 因为 Cube 在被 uninstall 时会被清理掉, 如果你用的是规范的 Saya Protocol 的话.""" 32 | 33 | module: str 34 | channel: Channel 35 | 36 | def __init__(self, module: str, channel: Channel) -> None: 37 | self.module = module 38 | self.channel = channel 39 | 40 | class Dispatcher(BaseDispatcher): 41 | @staticmethod 42 | async def catch(interface: "DispatcherInterface[SayaModuleUninstall]"): 43 | from graia.saya import Saya 44 | 45 | if interface.annotation is Saya: 46 | return saya_instance.get() 47 | elif interface.annotation is Channel: 48 | return interface.event.channel 49 | 50 | 51 | class SayaModuleUninstalled(Dispatchable): 52 | """不用返回 RemoveMe, 因为 Cube 在被 uninstall 时会被清理掉, 如果你用的是规范的 Saya Protocol 的话.""" 53 | 54 | module: str 55 | 56 | def __init__(self, module: str) -> None: 57 | self.module = module 58 | 59 | class Dispatcher(BaseDispatcher): 60 | @staticmethod 61 | async def catch(interface: "DispatcherInterface[SayaModuleUninstalled]"): 62 | from graia.saya import Saya 63 | 64 | if interface.annotation is Saya: 65 | return saya_instance.get() 66 | -------------------------------------------------------------------------------- /src/graia/saya/factory.py: -------------------------------------------------------------------------------- 1 | import functools 2 | from typing import Any, Callable, Dict, TypeVar 3 | 4 | from typing_extensions import ParamSpec 5 | 6 | from .channel import Channel 7 | from .cube import Cube 8 | from .schema import BaseSchema 9 | 10 | 11 | def ensure_buffer(func: Callable) -> Dict[str, Any]: 12 | if not hasattr(func, "__schema_buffer__"): 13 | setattr(func, "__schema_buffer__", {}) 14 | return getattr(func, "__schema_buffer__") 15 | 16 | 17 | P = ParamSpec("P") 18 | T_Callable = TypeVar("T_Callable", bound=Callable) 19 | SchemaWrapper = Callable[[Callable, Dict[str, Any]], BaseSchema] 20 | BufferModifier = Callable[[Dict[str, Any]], None] 21 | Wrapper = Callable[[T_Callable], T_Callable] 22 | 23 | 24 | def factory(func: Callable[P, SchemaWrapper]) -> Callable[P, Wrapper]: 25 | @functools.wraps(func) 26 | def wrapper(*args: P.args, **kwargs: P.kwargs) -> Wrapper: 27 | wrapper: SchemaWrapper = func(*args, **kwargs) 28 | 29 | def register(func: T_Callable) -> T_Callable: 30 | schema: BaseSchema = wrapper(func, ensure_buffer(func)) 31 | Channel.current().content.append(Cube(func, schema)) 32 | return func 33 | 34 | return register 35 | 36 | return wrapper 37 | 38 | 39 | def buffer_modifier(func: Callable[P, BufferModifier]) -> Callable[P, Wrapper]: 40 | @functools.wraps(func) 41 | def wrapper(*args: P.args, **kwargs: P.kwargs) -> Wrapper: 42 | modifier: BufferModifier = func(*args, **kwargs) 43 | 44 | def modify(func: T_Callable) -> T_Callable: 45 | modifier(ensure_buffer(func)) 46 | return func 47 | 48 | return modify 49 | 50 | return wrapper 51 | -------------------------------------------------------------------------------- /src/graia/saya/schema.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | 3 | from .context import channel_instance 4 | 5 | if TYPE_CHECKING: 6 | from .channel import Channel 7 | 8 | 9 | class BaseSchema: 10 | channel: "Channel" 11 | 12 | def __post_init__(self): 13 | self.channel = channel_instance.get() 14 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | from graia.saya import Channel, Saya 2 | from graia.saya.builtins.broadcast.schema import ListenerSchema 3 | from graia.saya.event import SayaModuleInstalled 4 | from creart import it 5 | 6 | saya = it(Saya) 7 | 8 | with saya.main_context() as channel: 9 | 10 | @channel.scoped_context 11 | class context1: 12 | a: str 13 | 14 | @channel.use(ListenerSchema(listening_events=[SayaModuleInstalled])) 15 | async def prepare(self, event: SayaModuleInstalled): 16 | self.a = "1" 17 | --------------------------------------------------------------------------------