├── .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 |
--------------------------------------------------------------------------------