├── ruff.toml ├── src ├── graia │ └── broadcast │ │ ├── builtin │ │ ├── __init__.py │ │ ├── defer.py │ │ ├── decorators.py │ │ ├── derive.py │ │ ├── event.py │ │ └── depend.py │ │ ├── entities │ │ ├── __init__.py │ │ ├── event.py │ │ ├── decorator.py │ │ ├── namespace.py │ │ ├── exectarget.py │ │ ├── signatures.py │ │ ├── dispatcher.pyi │ │ ├── dispatcher.py │ │ └── listener.py │ │ ├── interfaces │ │ ├── __init__.py │ │ ├── decorator.py │ │ └── dispatcher.py │ │ ├── priority.py │ │ ├── typing.py │ │ ├── exceptions.py │ │ ├── creator.py │ │ ├── interrupt │ │ ├── waiter.py │ │ └── __init__.py │ │ ├── utilles.py │ │ └── __init__.py ├── test │ ├── method.py │ ├── derive.py │ ├── postpone_annotation.py │ ├── get_throw.py │ ├── chain_post.py │ ├── dispatch.py │ └── deco.py ├── test_derive.py ├── test_nest.py ├── test_double.py └── test.py ├── pytest.ini ├── .pre-commit-config.yaml ├── .vscode └── settings.json ├── .github └── workflows │ └── pypi-publish.yml ├── example └── readme.py ├── README.md ├── pyproject.toml ├── .gitignore └── pdm.lock /ruff.toml: -------------------------------------------------------------------------------- 1 | line-length = 120 2 | -------------------------------------------------------------------------------- /src/graia/broadcast/builtin/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/graia/broadcast/entities/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/graia/broadcast/interfaces/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | python_files = src/test/*.py 3 | asyncio_mode = strict 4 | -------------------------------------------------------------------------------- /src/graia/broadcast/priority.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | 4 | class Priority(IntEnum): 5 | Special = -1 6 | Default = 16 7 | BuiltinListener = 8 8 | Logger = -2 9 | -------------------------------------------------------------------------------- /src/graia/broadcast/entities/event.py: -------------------------------------------------------------------------------- 1 | from typing import Type 2 | 3 | from .dispatcher import BaseDispatcher 4 | 5 | 6 | class Dispatchable: 7 | Dispatcher: Type[BaseDispatcher] 8 | 9 | 10 | BaseEvent = Dispatchable 11 | -------------------------------------------------------------------------------- /src/graia/broadcast/typing.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, Type, Union 2 | 3 | if TYPE_CHECKING: 4 | from graia.broadcast.entities.dispatcher import BaseDispatcher 5 | 6 | T_Dispatcher = Union[Type["BaseDispatcher"], "BaseDispatcher"] 7 | -------------------------------------------------------------------------------- /src/graia/broadcast/entities/decorator.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, Any, Callable 2 | 3 | if TYPE_CHECKING: 4 | from ..interfaces.decorator import DecoratorInterface 5 | 6 | 7 | class Decorator: 8 | target: Callable[["DecoratorInterface"], Any] 9 | pre: bool = False 10 | -------------------------------------------------------------------------------- /src/graia/broadcast/entities/namespace.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from typing import TYPE_CHECKING, List 3 | 4 | if TYPE_CHECKING: 5 | from ..typing import T_Dispatcher 6 | 7 | 8 | @dataclass 9 | class Namespace: 10 | name: str 11 | injected_dispatchers: List["T_Dispatcher"] = field(default_factory=list) 12 | 13 | priority: int = 0 14 | default: bool = False 15 | 16 | hide: bool = False 17 | disabled: bool = False 18 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/psf/black 3 | rev: 24.8.0 # Replace by any tag/version: https://github.com/psf/black/tags 4 | hooks: 5 | - id: black 6 | language_version: python3 7 | 8 | - repo: https://github.com/pycqa/isort 9 | rev: 5.13.2 10 | hooks: 11 | - id: isort 12 | name: isort (python) 13 | 14 | - repo: https://github.com/pre-commit/pre-commit-hooks 15 | rev: v4.6.0 16 | hooks: 17 | - id: end-of-file-fixer 18 | - id: trailing-whitespace 19 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.linting.enabled": false, 3 | "python.linting.pylintEnabled": true, 4 | "python.linting.banditEnabled": false, 5 | "maven.view": "hierarchical", 6 | "python.formatting.provider": "black", 7 | "python.pythonPath": "C:\\Users\\Chenw\\AppData\\Local\\pypoetry\\Cache\\virtualenvs\\graia-broadcast-kkIP7ti5-py3.8", 8 | "jupyter.jupyterServerType": "local", 9 | "cSpell.words": [ 10 | "Dispatchable", 11 | "oplog", 12 | "sourcery", 13 | "Unexisted", 14 | "utilles" 15 | ], 16 | "python.analysis.typeCheckingMode": "basic" 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/pypi-publish.yml: -------------------------------------------------------------------------------- 1 | name: Python package 2 | on: 3 | push: 4 | tags: 5 | - "v*.*.*" 6 | workflow_dispatch: 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/setup-python@v3 13 | name: Ensure Python Runtime 14 | with: 15 | python-version: '3.x' 16 | architecture: 'x64' 17 | - name: Ensure PDM & twine 18 | run: | 19 | python3 -m pip install pdm twine 20 | - name: Build Package 21 | run: | 22 | pdm build 23 | - name: Publish to PyPI 24 | run: | 25 | twine upload dist/* --non-interactive -u __token__ -p ${{ secrets.PYPI_TOKEN }} 26 | -------------------------------------------------------------------------------- /src/graia/broadcast/exceptions.py: -------------------------------------------------------------------------------- 1 | class OutOfMaxGenerater(Exception): 2 | pass 3 | 4 | 5 | class InvalidDispatcher(Exception): 6 | pass 7 | 8 | 9 | class RequirementCrashed(Exception): 10 | pass 11 | 12 | 13 | class DisabledNamespace(Exception): 14 | pass 15 | 16 | 17 | class ExistedNamespace(Exception): 18 | pass 19 | 20 | 21 | class UnexistedNamespace(Exception): 22 | pass 23 | 24 | 25 | class RegisteredEventListener(Exception): 26 | pass 27 | 28 | 29 | class InvalidEventName(Exception): 30 | pass 31 | 32 | 33 | class InvalidContextTarget(Exception): 34 | pass 35 | 36 | 37 | class PropagationCancelled(Exception): 38 | pass 39 | 40 | 41 | class ExecutionStop(Exception): 42 | pass 43 | -------------------------------------------------------------------------------- /src/graia/broadcast/entities/exectarget.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, Dict, Hashable, List, Optional 2 | 3 | from ..typing import T_Dispatcher 4 | from .decorator import Decorator 5 | 6 | 7 | class ExecTarget: 8 | callable: Callable 9 | dispatchers: List[T_Dispatcher] 10 | decorators: List[Decorator] 11 | 12 | oplog: Dict[Hashable, Dict[str, List[T_Dispatcher]]] 13 | 14 | def __init__( 15 | self, 16 | callable: Callable, 17 | inline_dispatchers: Optional[List[T_Dispatcher]] = None, 18 | decorators: Optional[List[Decorator]] = None, 19 | ): 20 | self.callable = callable 21 | self.dispatchers = inline_dispatchers or [] 22 | self.decorators = decorators or [] 23 | 24 | self.oplog = {} 25 | -------------------------------------------------------------------------------- /src/graia/broadcast/entities/signatures.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional, Union 2 | 3 | 4 | class ObjectContainer: 5 | target: Any 6 | 7 | def __init__(self, content: Optional[Union["ObjectContainer", Any]] = None): 8 | if content.__class__ is self.__class__: 9 | content = content.target # type: ignore 10 | self.target = content 11 | 12 | 13 | class Force(ObjectContainer): 14 | """用于转义在本框架中特殊部分的特殊值 15 | 16 | 例如:Dispatcher 返回时 None 表示本级 dispatcher 无法满足需求, 17 | DispatcherInterface 会继续向下查询, 18 | 而某些时候我们确实是需要传递 None 的, 19 | 这时候可以用本标识来保证 None 被顺利作为一个参数传入。 20 | """ 21 | 22 | 23 | class RemoveMe: 24 | """当本标识的实例为一受 Executor 影响的 Listener 返回值时, 25 | Executor 会尝试在当前 Broadcast 实例中找出并删除本 Listener 实例. 26 | """ 27 | 28 | def __new__(cls): 29 | return RemoveMe 30 | -------------------------------------------------------------------------------- /src/graia/broadcast/entities/dispatcher.pyi: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | from types import TracebackType 3 | from typing import List, Optional, Type, Union 4 | 5 | from ..interfaces.dispatcher import DispatcherInterface as DispatcherInterface 6 | 7 | class BaseDispatcher(metaclass=ABCMeta): 8 | mixin: List[Union[BaseDispatcher, Type[BaseDispatcher]]] 9 | @abstractmethod 10 | async def catch(self, interface: DispatcherInterface): ... 11 | def beforeExecution(self, interface: DispatcherInterface): ... 12 | def afterDispatch( 13 | self, 14 | interface: DispatcherInterface, 15 | exception: Optional[Exception], 16 | tb: Optional[TracebackType], 17 | ): ... 18 | def afterExecution( 19 | self, 20 | interface: DispatcherInterface, 21 | exception: Optional[Exception], 22 | tb: Optional[TracebackType], 23 | ): ... 24 | -------------------------------------------------------------------------------- /example/readme.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from graia.broadcast import Broadcast, Dispatchable 4 | from graia.broadcast.entities.dispatcher import BaseDispatcher 5 | from graia.broadcast.interfaces.dispatcher import DispatcherInterface 6 | 7 | 8 | class ExampleEvent(Dispatchable): 9 | class Dispatcher(BaseDispatcher): 10 | @staticmethod 11 | def catch(interface: "DispatcherInterface"): 12 | if interface.annotation is str: 13 | return "ok, i'm." 14 | 15 | 16 | loop = asyncio.get_event_loop() 17 | broadcast = Broadcast() 18 | 19 | 20 | @broadcast.receiver("ExampleEvent") # or just receiver(ExampleEvent) 21 | async def event_listener(maybe_you_are_str: str): 22 | print(maybe_you_are_str) # <<< ok, i'm 23 | 24 | 25 | async def main(): 26 | broadcast.postEvent(ExampleEvent()) # sync call is allowed. 27 | 28 | 29 | loop.run_until_complete(main()) 30 | -------------------------------------------------------------------------------- /src/test/method.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import pytest 4 | 5 | from graia.broadcast import Broadcast 6 | from graia.broadcast.entities.dispatcher import BaseDispatcher 7 | from graia.broadcast.entities.event import Dispatchable 8 | from graia.broadcast.entities.signatures import Force 9 | from graia.broadcast.exceptions import InvalidEventName 10 | from graia.broadcast.interfaces.dispatcher import DispatcherInterface 11 | 12 | 13 | class TestEvent(Dispatchable): 14 | class Dispatcher(BaseDispatcher): 15 | @staticmethod 16 | async def catch(interface: "DispatcherInterface"): 17 | if interface.name == "ster": 18 | return "1" 19 | 20 | 21 | class ChainEvent(TestEvent): ... 22 | 23 | 24 | class NestedEvent(ChainEvent): ... 25 | 26 | 27 | @pytest.mark.asyncio 28 | async def test_fail_lookup(): 29 | bcc = Broadcast() 30 | 31 | with pytest.raises(InvalidEventName): 32 | bcc.receiver("ImaginaryEvent")(lambda: ...) 33 | bcc.receiver("NestedEvent")(lambda: ...) 34 | -------------------------------------------------------------------------------- /src/graia/broadcast/builtin/defer.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | from types import TracebackType 5 | from typing import Awaitable, Callable 6 | 7 | from ..entities.dispatcher import BaseDispatcher 8 | from ..interfaces.dispatcher import DispatcherInterface 9 | 10 | 11 | class DeferDispatcher(BaseDispatcher): 12 | async def beforeExecution(self, interface: DispatcherInterface): 13 | interface.local_storage["defer_callbacks"] = [] 14 | 15 | async def catch(self, interface: DispatcherInterface): 16 | return 17 | 18 | async def afterExecution( 19 | self, 20 | interface: DispatcherInterface, 21 | exception: Exception | None, 22 | tb: TracebackType | None, 23 | ): 24 | callbacks: list[Callable[[DispatcherInterface, Exception | None, TracebackType | None], Awaitable[None]]] 25 | callbacks = interface.local_storage.get("defer_callbacks") # type: ignore 26 | 27 | if not callbacks: 28 | return 29 | 30 | await asyncio.wait([i(interface, exception, tb) for i in callbacks]) 31 | -------------------------------------------------------------------------------- /src/test_derive.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Annotated 3 | 4 | from graia.broadcast import Broadcast, Dispatchable 5 | from graia.broadcast.entities.dispatcher import BaseDispatcher 6 | from graia.broadcast.interfaces.dispatcher import DispatcherInterface 7 | 8 | 9 | class ExampleEvent(Dispatchable): 10 | class Dispatcher(BaseDispatcher): 11 | @staticmethod 12 | async def catch(interface: "DispatcherInterface"): 13 | if interface.annotation is str: 14 | return "ok, i'm." 15 | 16 | 17 | loop = asyncio.get_event_loop() 18 | broadcast = Broadcast() 19 | 20 | 21 | async def test_derive_1(v: str, dii: DispatcherInterface): 22 | print("in derive 1", v) 23 | return v[1:] 24 | 25 | 26 | @broadcast.receiver("ExampleEvent") # or just receiver(ExampleEvent) 27 | async def event_listener(maybe_you_are_str: Annotated[str, test_derive_1, test_derive_1]): 28 | print(maybe_you_are_str) # <<< ok, i'm 29 | 30 | 31 | async def main(): 32 | await broadcast.postEvent(ExampleEvent()) # sync call is allowed. 33 | 34 | 35 | loop.run_until_complete(main()) 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Broadcast Control for Graia Framework 2 | 3 | ## 这是什么? 4 | 一个高性能,高可扩展性,设计简洁,基于 `asyncio` 的事件系统,为 `Graia Framework` 设计。 5 | 6 | ## 安装 7 | ### 从 PyPI 安装 8 | ``` bash 9 | pip install graia-broadcast 10 | # 或者使用 poetry 11 | poetry add graia-broadcast 12 | ``` 13 | 14 | # Example 15 | 16 | ```python 17 | from graia.broadcast import Dispatchable, BaseDispatcher, Broadcast 18 | from graia.broadcast.interfaces.dispatcher import DispatcherInterface 19 | 20 | class ExampleEvent(Dispatchable): 21 | class Dispatcher(BaseDispatcher): 22 | def catch(interface: "DispatcherInterface"): 23 | if interface.annotation is str: 24 | return "ok, i'm." 25 | 26 | broadcast = Broadcast() 27 | 28 | @broadcast.receiver("ExampleEvent") # or just receiver(ExampleEvent) 29 | async def event_listener(maybe_you_are_str: str): 30 | print(maybe_you_are_str) # <<< ok, i'm 31 | 32 | async def main(): 33 | broadcast.postEvent(ExampleEvent()) # sync call is allowed. 34 | await asyncio.sleep(0.1) # to solve event task. 35 | 36 | loop.run_until_complete(main()) 37 | ``` 38 | 39 | ## 开源协议 40 | 本实现以 MIT 为开源协议。 41 | -------------------------------------------------------------------------------- /src/graia/broadcast/entities/dispatcher.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | from typing import TYPE_CHECKING, List, Type, Union 3 | 4 | if TYPE_CHECKING: 5 | from ..interfaces.dispatcher import DispatcherInterface 6 | 7 | 8 | class BaseDispatcher(metaclass=ABCMeta): 9 | """所有非单函数型 Dispatcher 的基类, 用于为参数解析提供可扩展的支持.""" 10 | 11 | mixin: List[Union["BaseDispatcher", Type["BaseDispatcher"]]] 12 | """声明该 Dispatcher 所包含的来自其他 Dispatcher 提供的参数解析支持, 13 | 若某参数该 Dispatcher 无法解析, 将跳转到该列表中并交由其中的 Dispatcher 进行解析, 14 | 该列表中的 Dispatcher 全部被调用过且都不返回一有效值时才会将解析权交由其他的 Dispatcher. 15 | """ 16 | 17 | @abstractmethod 18 | async def catch(self, interface: "DispatcherInterface"): 19 | """该方法可以是 `staticmethod`, `classmethod` 亦或是普通的方法/函数. 20 | 唯一的要求是 `Dispatcher.catch` 获取到的必须为一可调用异步 Callable. 21 | 22 | Args: 23 | interface (DispatcherInterface): `Dispatcher` 服务的主要对象, 可以从其中获取以下信息: 24 | - 当前解析中的参数的信息; 25 | - 当前执行的信息, 比如正在处理的事件, `Listener`/`ExecTarget` etc.; 26 | """ 27 | pass 28 | 29 | beforeExecution = None 30 | afterDispatch = None 31 | afterExecution = None 32 | -------------------------------------------------------------------------------- /src/graia/broadcast/builtin/decorators.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from typing import Any, Optional, Union 3 | 4 | from ..entities.decorator import Decorator 5 | from ..entities.signatures import Force 6 | from ..exceptions import RequirementCrashed 7 | from ..interfaces.decorator import DecoratorInterface 8 | from .depend import Depend as Depend 9 | 10 | 11 | class OptionalParam(Decorator): 12 | pre = True 13 | 14 | def __init__(self, origin: Any): 15 | self.origin = origin 16 | 17 | async def target(self, interface: DecoratorInterface) -> Optional[Any]: 18 | annotation = interface.annotation 19 | if typing.get_origin(annotation) is Union: 20 | annotation = Union[tuple(x for x in typing.get_args(annotation) if x not in (None, type(None)))] # type: ignore 21 | try: 22 | return Force( 23 | await interface.dispatcher_interface.lookup_by_directly( 24 | interface, 25 | interface.dispatcher_interface.name, 26 | annotation, 27 | self.origin, 28 | ) 29 | ) 30 | except RequirementCrashed: 31 | return Force() 32 | -------------------------------------------------------------------------------- /src/graia/broadcast/builtin/derive.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Protocol, TypeVar 4 | 5 | from ..entities.dispatcher import BaseDispatcher 6 | from ..entities.signatures import ObjectContainer 7 | from ..interfaces.dispatcher import DispatcherInterface 8 | 9 | try: 10 | from typing_extensions import get_args 11 | except ImportError: 12 | from typing import get_args 13 | 14 | 15 | T = TypeVar("T") 16 | 17 | 18 | class Derive(Protocol[T]): 19 | async def __call__(self, value: T, dispatcher_interface: DispatcherInterface) -> T: ... 20 | 21 | 22 | class Origin(ObjectContainer): 23 | """直接为 Derive 指定 Origin Type, 覆盖原本从形参中获取的 Origin Type.""" 24 | 25 | 26 | class DeriveDispatcher(BaseDispatcher): 27 | async def catch(self, interface: DispatcherInterface): 28 | if not interface.is_annotated: 29 | return 30 | args = get_args(interface.annotation) 31 | origin_arg, meta = args[0], args[1:] 32 | if meta and isinstance(meta[0], Origin): 33 | origin_arg = meta[0].target 34 | meta = meta[1:] 35 | result = await interface.lookup_param(interface.name, origin_arg, interface.default) 36 | for i in meta: 37 | result = await i(result, interface) 38 | return result 39 | -------------------------------------------------------------------------------- /src/graia/broadcast/entities/listener.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Callable, Dict, List, Optional, Type 4 | 5 | from ..typing import T_Dispatcher 6 | from .decorator import Decorator 7 | from .event import Dispatchable 8 | from .exectarget import ExecTarget 9 | from .namespace import Namespace 10 | 11 | 12 | class Listener(ExecTarget): 13 | namespace: Namespace 14 | listening_events: List[Type[Dispatchable]] 15 | priorities: Dict[Type[Dispatchable] | None, int] 16 | 17 | def __init__( 18 | self, 19 | callable: Callable, 20 | namespace: Namespace, 21 | listening_events: List[Type[Dispatchable]], 22 | inline_dispatchers: Optional[List[T_Dispatcher]] = None, 23 | decorators: Optional[List[Decorator]] = None, 24 | priority: int = 16, 25 | ) -> None: 26 | super().__init__(callable, inline_dispatchers, decorators) 27 | 28 | self.namespace = namespace 29 | self.listening_events = listening_events 30 | self.priorities = {None: priority} 31 | 32 | @property 33 | def priority(self) -> int: 34 | return self.priorities[None] 35 | 36 | def add_priority(self, event: Type[Dispatchable], priority: int) -> None: 37 | self.priorities[event] = priority 38 | -------------------------------------------------------------------------------- /src/graia/broadcast/creator.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | from creart import AbstractCreator, CreateTargetInfo, it 6 | 7 | if TYPE_CHECKING: 8 | from . import Broadcast 9 | from .interrupt import InterruptControl 10 | 11 | 12 | class BroadcastCreator(AbstractCreator): 13 | targets = ( 14 | CreateTargetInfo( 15 | module="graia.broadcast", 16 | identify="Broadcast", 17 | humanized_name="Broadcast Control", 18 | description=" a high performance, highly customizable, elegantly designed event system based on asyncio", 19 | author=["GraiaProject@github"], 20 | ), 21 | CreateTargetInfo( 22 | module="graia.broadcast.interrupt", 23 | identify="InterruptControl", 24 | humanized_name="Interrupt", 25 | description=" Interrupt feature for broadcast control.", 26 | author=["GraiaProject@github"], 27 | ), 28 | ) 29 | 30 | @staticmethod 31 | def create( 32 | create_type: type[Broadcast | InterruptControl], 33 | ) -> Broadcast | InterruptControl: 34 | from . import Broadcast 35 | from .interrupt import InterruptControl 36 | 37 | if issubclass(create_type, Broadcast): 38 | return create_type() 39 | elif issubclass(create_type, InterruptControl): 40 | return create_type(it(Broadcast)) 41 | -------------------------------------------------------------------------------- /src/test/derive.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Annotated 3 | 4 | import pytest 5 | 6 | from graia.broadcast import Broadcast, Dispatchable 7 | from graia.broadcast.builtin.derive import Origin 8 | from graia.broadcast.entities.dispatcher import BaseDispatcher 9 | from graia.broadcast.interfaces.dispatcher import DispatcherInterface 10 | 11 | 12 | class ExampleEvent(Dispatchable): 13 | class Dispatcher(BaseDispatcher): 14 | @staticmethod 15 | async def catch(interface: "DispatcherInterface"): 16 | if interface.annotation is str: 17 | return "ok, i'm." 18 | 19 | 20 | @pytest.mark.asyncio 21 | async def test_derive(): 22 | broadcast = Broadcast() 23 | 24 | l = [] 25 | 26 | async def derive_fun(v: str, dii: DispatcherInterface): 27 | assert dii.name == "string" 28 | assert dii.is_annotated 29 | assert dii.annotated_origin == str 30 | assert dii.annotated_metadata[-2:] == (derive_fun, derive_fun) 31 | return v[1:] 32 | 33 | @broadcast.receiver("ExampleEvent") # or just receiver(ExampleEvent) 34 | async def _(string: Annotated[str, derive_fun, derive_fun]): 35 | l.append(string == ", i'm.") 36 | 37 | @broadcast.receiver("ExampleEvent") # or just receiver(ExampleEvent) 38 | async def _(string: Annotated[str, Origin(str), derive_fun, derive_fun]): 39 | l.append(string == ", i'm.") 40 | 41 | await broadcast.postEvent(ExampleEvent()) # sync call is allowed. 42 | assert l == [True, True] 43 | -------------------------------------------------------------------------------- /src/test/postpone_annotation.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | 5 | import pytest 6 | 7 | from graia.broadcast import Broadcast 8 | from graia.broadcast.entities.dispatcher import BaseDispatcher 9 | from graia.broadcast.entities.event import Dispatchable 10 | from graia.broadcast.entities.signatures import Force 11 | from graia.broadcast.interfaces.dispatcher import DispatcherInterface 12 | 13 | 14 | class TestDispatcher(BaseDispatcher): 15 | @staticmethod 16 | async def catch(interface: DispatcherInterface): 17 | if interface.name == "p": 18 | return "P_dispatcher" 19 | elif interface.name == "f": 20 | return Force(2) 21 | 22 | 23 | class TestEvent(Dispatchable): 24 | class Dispatcher(BaseDispatcher): 25 | @staticmethod 26 | async def catch(interface: "DispatcherInterface"): 27 | if interface.name == "ster": 28 | return "1" 29 | if interface.name == "p": 30 | return "P_event" 31 | 32 | 33 | @pytest.mark.asyncio 34 | async def test_event_dispatch(): 35 | bcc = Broadcast() 36 | 37 | executed = [] 38 | 39 | @bcc.receiver(TestEvent) 40 | async def _1(ster, p, b: Broadcast, i: DispatcherInterface): 41 | assert ster == "1" 42 | assert p == "P_event" 43 | assert b is bcc 44 | assert i.__class__ == DispatcherInterface 45 | executed.append(1) 46 | 47 | await bcc.postEvent(TestEvent()) 48 | 49 | assert len(executed) == 1 50 | -------------------------------------------------------------------------------- /src/test/get_throw.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import pytest 4 | 5 | from graia.broadcast import Broadcast 6 | from graia.broadcast.builtin.event import EventExceptionThrown 7 | from graia.broadcast.entities.dispatcher import BaseDispatcher 8 | from graia.broadcast.entities.event import Dispatchable 9 | from graia.broadcast.interfaces.dispatcher import DispatcherInterface 10 | 11 | 12 | class TestDispatcher(BaseDispatcher): 13 | @staticmethod 14 | async def catch(interface: DispatcherInterface): 15 | if interface.name == "p": 16 | return 1 17 | 18 | 19 | class TestEvent(Dispatchable): 20 | class Dispatcher(BaseDispatcher): 21 | @staticmethod 22 | async def catch(interface: "DispatcherInterface"): 23 | if interface.name == "ster": 24 | return "1" 25 | 26 | 27 | @pytest.mark.asyncio 28 | async def test_get_exc(): 29 | bcc = Broadcast() 30 | executed = [] 31 | 32 | @bcc.receiver(TestEvent) 33 | async def _(): 34 | executed.append(1) 35 | raise Exception("test") 36 | 37 | @bcc.receiver(EventExceptionThrown, dispatchers=[TestDispatcher]) 38 | async def _(ev: EventExceptionThrown, event, exc: Exception, p): 39 | executed.append(1) 40 | assert ev.event.__class__ == event.__class__ == TestEvent 41 | assert ev.exception.__class__ == exc.__class__ == Exception 42 | assert ev.exception.args == ("test",) 43 | assert p == 1 44 | executed.append(1) 45 | 46 | await bcc.postEvent(TestEvent()) 47 | 48 | assert len(executed) == 3 49 | -------------------------------------------------------------------------------- /src/graia/broadcast/builtin/event.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | from typing import TYPE_CHECKING, Optional 3 | 4 | from ..entities.dispatcher import BaseDispatcher 5 | from ..entities.event import Dispatchable 6 | 7 | if TYPE_CHECKING: 8 | from graia.broadcast.interfaces.dispatcher import DispatcherInterface 9 | 10 | 11 | class ExceptionThrown(Dispatchable): 12 | exception: Exception 13 | event: Optional[Dispatchable] 14 | 15 | def __init__(self, exception: Exception, event: Optional[Dispatchable]) -> None: 16 | self.exception = exception 17 | self.event = event 18 | 19 | class Dispatcher(BaseDispatcher): 20 | @staticmethod 21 | async def catch(interface: "DispatcherInterface[ExceptionThrown]"): 22 | event: ExceptionThrown = interface.event 23 | if event.event is not None: 24 | with contextlib.suppress(TypeError): 25 | if interface.name == "event" or isinstance(interface.event.event, interface.annotation): 26 | return interface.event.event 27 | if interface.name == "exception" or isinstance(interface.event.exception, interface.annotation): 28 | return interface.event.exception 29 | 30 | 31 | class EventExceptionThrown(ExceptionThrown): 32 | event: Dispatchable 33 | 34 | def __init__(self, exception: Exception, event: Dispatchable) -> None: 35 | self.exception = exception 36 | self.event = event 37 | 38 | 39 | ExceptionThrowed = EventExceptionThrown # backward compatibility 40 | -------------------------------------------------------------------------------- /src/test/chain_post.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import pytest 4 | 5 | from graia.broadcast import Broadcast 6 | from graia.broadcast.entities.dispatcher import BaseDispatcher 7 | from graia.broadcast.entities.event import Dispatchable 8 | from graia.broadcast.interfaces.dispatcher import DispatcherInterface 9 | 10 | 11 | class TestDispatcher(BaseDispatcher): 12 | @staticmethod 13 | async def catch(interface: DispatcherInterface): 14 | if interface.name == "ster": 15 | return 1 16 | 17 | 18 | class TestEvent1(Dispatchable): 19 | class Dispatcher(BaseDispatcher): 20 | @staticmethod 21 | async def catch(interface: "DispatcherInterface"): 22 | if interface.name == "ster": 23 | return "1" 24 | elif interface.name == "ster1": 25 | return "res_ster_1" 26 | 27 | 28 | class TestEvent2(Dispatchable): 29 | class Dispatcher(BaseDispatcher): 30 | @staticmethod 31 | async def catch(interface: "DispatcherInterface"): 32 | if interface.name == "ster": 33 | return "res_ster" 34 | 35 | 36 | @pytest.mark.asyncio 37 | async def test_(): 38 | event = TestEvent1() 39 | 40 | broadcast = Broadcast() 41 | 42 | finish = [] 43 | 44 | @broadcast.receiver(TestEvent1) 45 | async def s(e: TestEvent1, ster, ster1): 46 | assert ster == "1" 47 | assert ster1 == "res_ster_1" 48 | broadcast.postEvent(TestEvent2(), e) 49 | 50 | @broadcast.receiver(TestEvent2) 51 | async def t(e: TestEvent2, ster, ster1): 52 | assert isinstance(e, TestEvent2) 53 | assert ster == "res_ster" 54 | assert ster1 == "res_ster_1" 55 | finish.append(1) 56 | 57 | await broadcast.postEvent(event) 58 | 59 | assert finish 60 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | # PEP 621 project metadata 3 | # See https://www.python.org/dev/peps/pep-0621/ 4 | authors = [{ name = "GreyElaina", email = "GreyElaina@outlook.com" }] 5 | license = { text = "MIT" } 6 | requires-python = ">=3.8,<4.0" 7 | dependencies = [ 8 | "typing-extensions>=3.10.0; python_version < \"3.9\"", 9 | "creart~=0.3.0", 10 | ] 11 | name = "graia-broadcast" 12 | version = "0.24.0" 13 | description = "a highly customizable, elegantly designed event system based on asyncio" 14 | 15 | [tool.pdm.build] 16 | includes = ["src/graia"] 17 | 18 | [tool.pdm.dev-dependencies] 19 | dev = [ 20 | "black<23.0.0,>=22.1.0", 21 | "pre-commit", 22 | "flake8<5.0.0,>=4.0.1", 23 | "isort<6.0.0,>=5.10.1", 24 | "pytest<8.0.0,>=7.0.1", 25 | "coverage<7.0.0,>=6.3.2", 26 | "pytest-asyncio<1.0.0,>=0.18.2", 27 | ] 28 | 29 | [project.entry-points."creart.creators"] 30 | broadcast = "graia.broadcast.creator:BroadcastCreator" 31 | 32 | [build-system] 33 | requires = ["pdm-backend"] 34 | build-backend = "pdm.backend" 35 | 36 | [tool.black] 37 | line-length = 120 38 | 39 | [tool.isort] 40 | profile = "black" 41 | 42 | [tool.coverage.run] 43 | branch = true 44 | omit = ["*/test/*"] 45 | 46 | [tool.coverage.report] 47 | # Regexes for lines to exclude from consideration 48 | exclude_lines = [ 49 | # standard pragma 50 | "pragma: no cover", 51 | # Don't complain if non-runnable code isn't run: 52 | "if 0:", 53 | "if __name__ == .__main__.:", 54 | "if (typing\\.)?TYPE_CHECKING( is True)?:", 55 | "\\.\\.\\.", 56 | "pass", 57 | # Don't complain about abstract methods, they aren't run: 58 | "@(abc\\.)?abstractmethod", 59 | # Don't complain overload method / functions 60 | "@(typing\\.)?overload", 61 | # don't complain __repr__ and __str__ and __repr_args__ for representation 62 | "def __repr__", 63 | "def __str__", 64 | "def __repr_args__", 65 | "except ImportError:", # Don't complain about import fallback 66 | ] 67 | 68 | [tool.pyright] 69 | pythonVersion = "3.7" 70 | -------------------------------------------------------------------------------- /src/test_nest.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from graia.broadcast import Broadcast 4 | from graia.broadcast.builtin.decorators import Depend 5 | from graia.broadcast.entities.dispatcher import BaseDispatcher 6 | from graia.broadcast.entities.event import Dispatchable 7 | from graia.broadcast.interfaces.dispatcher import DispatcherInterface 8 | 9 | 10 | class TestDispatcher(BaseDispatcher): 11 | @classmethod 12 | async def beforeExecution(cls, interface: DispatcherInterface): 13 | if interface.depth == 0: 14 | print("beforeExecution") 15 | 16 | @classmethod 17 | async def catch(cls, interface: DispatcherInterface): 18 | if interface.name == "ster": 19 | return 1 20 | 21 | 22 | class TestEvent(Dispatchable): 23 | class Dispatcher(BaseDispatcher): 24 | mixin = [TestDispatcher] 25 | 26 | @classmethod 27 | async def catch(cls, interface: "DispatcherInterface"): 28 | if interface.name == "ster1": 29 | return "1" 30 | elif interface.name == "ster2": 31 | return 54352345 32 | 33 | @classmethod 34 | async def afterExecution( 35 | self, 36 | interface: DispatcherInterface, 37 | *args, 38 | ): 39 | if interface.depth == 0: 40 | print("afterExecution") 41 | 42 | 43 | event = TestEvent() 44 | loop = asyncio.new_event_loop() 45 | 46 | broadcast = Broadcast() 47 | 48 | 49 | @broadcast.receiver( 50 | TestEvent, 51 | decorators=[ 52 | Depend(lambda ster: print("depend: ster", ster)), 53 | Depend(lambda ster1: print("depend: ster1", ster1)), 54 | Depend(lambda ster2: print("depend: ster2", ster2)), 55 | ], 56 | ) 57 | async def s(e: TestEvent): 58 | print(e) 59 | 60 | 61 | def error(a: int): 62 | raise ValueError("error", a) 63 | 64 | 65 | @broadcast.receiver(TestEvent, decorators=[Depend(error)]) 66 | async def s1(e: TestEvent): 67 | print(e) 68 | 69 | 70 | loop.run_until_complete(asyncio.wait([broadcast.postEvent(event)])) 71 | -------------------------------------------------------------------------------- /src/test_double.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | # import objgraph 4 | # import copy 5 | import functools 6 | import random 7 | import sys 8 | import time 9 | from typing import Any, Generator, Tuple, Union 10 | 11 | from graia.broadcast import Broadcast 12 | from graia.broadcast.builtin.decorators import Depend 13 | from graia.broadcast.entities.decorator import Decorator 14 | from graia.broadcast.entities.dispatcher import BaseDispatcher 15 | from graia.broadcast.entities.event import Dispatchable 16 | from graia.broadcast.entities.exectarget import ExecTarget 17 | from graia.broadcast.entities.listener import Listener 18 | from graia.broadcast.exceptions import ExecutionStop, PropagationCancelled 19 | from graia.broadcast.interfaces.decorator import DecoratorInterface 20 | from graia.broadcast.interfaces.dispatcher import DispatcherInterface 21 | from graia.broadcast.interrupt import InterruptControl 22 | from graia.broadcast.interrupt.waiter import Waiter 23 | from graia.broadcast.utilles import dispatcher_mixin_handler 24 | 25 | 26 | class TestDispatcher(BaseDispatcher): 27 | @staticmethod 28 | async def catch(interface: DispatcherInterface): 29 | if interface.name == "ster": 30 | return 1 31 | 32 | 33 | class TestEvent1(Dispatchable): 34 | class Dispatcher(BaseDispatcher): 35 | @staticmethod 36 | async def catch(interface: "DispatcherInterface"): 37 | if interface.name == "ster": 38 | return "1" 39 | elif interface.name == "ster1": 40 | return 54352345 41 | 42 | 43 | class TestEvent2(Dispatchable): 44 | class Dispatcher(BaseDispatcher): 45 | @staticmethod 46 | async def catch(interface: "DispatcherInterface"): 47 | if interface.name == "ster": 48 | return 6546 49 | 50 | 51 | event = TestEvent1() 52 | loop = asyncio.new_event_loop() 53 | 54 | broadcast = Broadcast() 55 | 56 | 57 | @broadcast.receiver(TestEvent1) 58 | async def s(e: TestEvent1): 59 | broadcast.postEvent(TestEvent2(), e) 60 | 61 | 62 | @broadcast.receiver(TestEvent2) 63 | async def t(e: TestEvent2, ster, ster1): 64 | print(e, ster, ster1) 65 | 66 | 67 | loop.run_until_complete(asyncio.wait([broadcast.postEvent(event)])) 68 | -------------------------------------------------------------------------------- /src/graia/broadcast/interfaces/decorator.py: -------------------------------------------------------------------------------- 1 | from contextlib import AsyncExitStack 2 | from types import TracebackType 3 | from typing import TYPE_CHECKING, Optional, cast 4 | 5 | from ..entities.decorator import Decorator 6 | from ..entities.dispatcher import BaseDispatcher 7 | from ..entities.signatures import Force 8 | from ..utilles import Ctx, run_always_await 9 | 10 | if TYPE_CHECKING: 11 | from ..interfaces.dispatcher import DispatcherInterface 12 | 13 | 14 | ctx_dei_returnvalue = Ctx("ctx_dei_returnvalue") 15 | 16 | 17 | class DecoratorInterface(BaseDispatcher): 18 | """Broadcast Control 内部机制 Decorator 的具体管理实现""" 19 | 20 | @property 21 | def dispatcher_interface(self) -> "DispatcherInterface": 22 | from .dispatcher import DispatcherInterface 23 | 24 | return DispatcherInterface.ctx.get() 25 | 26 | @property 27 | def name(self): 28 | return self.dispatcher_interface.name 29 | 30 | @property 31 | def annotation(self): 32 | return self.dispatcher_interface.annotation 33 | 34 | @property 35 | def event(self): 36 | return self.dispatcher_interface.event 37 | 38 | @property 39 | def return_value(self): 40 | return ctx_dei_returnvalue.get() 41 | 42 | @property 43 | def local_storage(self): 44 | return self.dispatcher_interface.local_storage 45 | 46 | async def catch(self, interface: "DispatcherInterface"): 47 | if isinstance(interface.default, Decorator): 48 | decorator: Decorator = interface.default 49 | with ctx_dei_returnvalue.use( 50 | await interface.lookup_param(interface.name, interface.annotation, None) if not decorator.pre else None 51 | ): 52 | return Force(await run_always_await(decorator.target, self)) 53 | 54 | async def afterExecution( 55 | self, interface: "DispatcherInterface", exception: Optional[Exception], tb: Optional[TracebackType] 56 | ): 57 | stack = cast("AsyncExitStack | None", interface.local_storage.get("_depend_astack")) 58 | 59 | if stack is None: 60 | return 61 | 62 | if exception: 63 | await stack.__aexit__(type(exception), exception, tb) 64 | else: 65 | await stack.aclose() 66 | -------------------------------------------------------------------------------- /src/graia/broadcast/interrupt/waiter.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | from typing import Any, Callable, List, Optional, Type 3 | 4 | from ..entities.decorator import Decorator 5 | from ..entities.event import Dispatchable 6 | from ..typing import T_Dispatcher 7 | 8 | 9 | class Waiter(metaclass=ABCMeta): 10 | listening_events: List[Type[Dispatchable]] 11 | using_dispatchers: List[T_Dispatcher] 12 | using_decorators: List[Decorator] 13 | priority: int 14 | block_propagation: bool 15 | detected_event: Callable[..., Any] 16 | 17 | @classmethod 18 | def create( 19 | cls, 20 | listening_events: List[Type[Dispatchable]], 21 | using_dispatchers: Optional[List[T_Dispatcher]] = None, 22 | using_decorators: Optional[List[Decorator]] = None, 23 | priority: int = 15, # 默认情况下都是需要高于默认 16 的监听吧... 24 | block_propagation: bool = False, 25 | ) -> Type["Waiter"]: 26 | async def detected_event(self) -> Any: 27 | pass 28 | 29 | return type( 30 | "AbstractWaiter", 31 | (cls,), # type: ignore 32 | { 33 | "listening_events": listening_events, 34 | "using_dispatchers": using_dispatchers, 35 | "using_decorators": using_decorators, 36 | "priority": priority, 37 | "block_propagation": block_propagation, 38 | "detected_event": abstractmethod(detected_event), 39 | }, 40 | ) 41 | 42 | @classmethod 43 | def create_using_function( 44 | cls, 45 | listening_events: List[Type[Dispatchable]], 46 | using_dispatchers: Optional[List[T_Dispatcher]] = None, 47 | using_decorators: Optional[List[Decorator]] = None, 48 | priority: int = 15, # 默认情况下都是需要高于默认 16 的监听吧... 49 | block_propagation: bool = False, 50 | ): 51 | def wrapper(func): 52 | return type( 53 | "SingleWaiter", 54 | ( 55 | cls.create( 56 | listening_events, 57 | using_dispatchers, 58 | using_decorators, 59 | priority, 60 | block_propagation, 61 | ), 62 | ), 63 | {"detected_event": staticmethod(func)}, 64 | )() 65 | 66 | return wrapper 67 | -------------------------------------------------------------------------------- /src/test.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | # import objgraph 4 | # import copy 5 | import functools 6 | import random 7 | import sys 8 | import time 9 | from typing import Any, Generator, Tuple, Union 10 | 11 | from graia.broadcast import Broadcast 12 | from graia.broadcast.builtin.decorators import Depend 13 | from graia.broadcast.entities.decorator import Decorator 14 | from graia.broadcast.entities.dispatcher import BaseDispatcher 15 | from graia.broadcast.entities.event import Dispatchable 16 | from graia.broadcast.entities.exectarget import ExecTarget 17 | from graia.broadcast.entities.listener import Listener 18 | from graia.broadcast.exceptions import ExecutionStop, PropagationCancelled 19 | from graia.broadcast.interfaces.decorator import DecoratorInterface 20 | from graia.broadcast.interfaces.dispatcher import DispatcherInterface 21 | from graia.broadcast.interrupt import InterruptControl 22 | from graia.broadcast.interrupt.waiter import Waiter 23 | from graia.broadcast.utilles import dispatcher_mixin_handler 24 | 25 | 26 | class TestDispatcher(BaseDispatcher): 27 | @staticmethod 28 | async def catch(interface: DispatcherInterface): 29 | if interface.name == "ster": 30 | return 1 31 | 32 | 33 | class TestEvent(Dispatchable): 34 | class Dispatcher(BaseDispatcher): 35 | @staticmethod 36 | async def catch(interface: "DispatcherInterface"): 37 | if interface.name == "ster": 38 | return "1" 39 | 40 | 41 | class AsInt(Decorator): 42 | o: int = 0 43 | 44 | async def target(self, interface: "DecoratorInterface"): 45 | self.o += 1 46 | return int(interface.return_value) 47 | 48 | 49 | event = TestEvent() 50 | loop = asyncio.get_event_loop() 51 | 52 | broadcast = Broadcast() 53 | 54 | p = AsInt() 55 | 56 | 57 | @broadcast.receiver(TestEvent) 58 | async def r(ster): 59 | pass 60 | 61 | 62 | count = 100000 63 | 64 | event = TestEvent() 65 | listener = broadcast.getListener(r) 66 | assert listener is not None 67 | tasks = [] 68 | import cProfile 69 | 70 | mixins = dispatcher_mixin_handler(event.Dispatcher) 71 | for _ in range(count): 72 | # broadcast.postEvent(event) 73 | # tasks.append( 74 | # loop.create_task(broadcast.Executor(listener, event))) 75 | tasks.append(broadcast.Executor(listener, dispatchers=mixins.copy())) 76 | 77 | s = time.time() 78 | # print(s) 79 | # cProfile.run("loop.run_until_complete(asyncio.gather(*tasks))") 80 | 81 | 82 | async def main(): 83 | await asyncio.gather(*tasks) 84 | 85 | 86 | asyncio.run(main()) 87 | 88 | e = time.time() 89 | n1 = e - s 90 | 91 | s2 = time.time() 92 | # loop.run_until_complete(asyncio.gather(*[r(1) for _ in range(count)])) 93 | e2 = time.time() 94 | n2 = e2 - s2 95 | 96 | 97 | # loop.run_until_complete(asyncio.sleep(0.1)) 98 | print(n1, count, n2) 99 | print(f"used {n1}, {count/n1}o/s,") 100 | print(listener.oplog) 101 | print(p.o) 102 | # print(tasks) 103 | -------------------------------------------------------------------------------- /src/graia/broadcast/builtin/depend.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from contextlib import AsyncExitStack, asynccontextmanager, contextmanager 4 | from inspect import isasyncgenfunction, isgeneratorfunction 5 | from types import TracebackType 6 | from typing import AsyncContextManager, Callable, ContextManager 7 | 8 | from graia.broadcast.interfaces.dispatcher import ( 9 | DispatcherInterface as DispatcherInterface, 10 | ) 11 | 12 | from ..entities.decorator import Decorator 13 | from ..entities.dispatcher import BaseDispatcher 14 | from ..entities.exectarget import ExecTarget 15 | from ..entities.signatures import Force 16 | from ..interfaces.decorator import DecoratorInterface 17 | from ..interfaces.dispatcher import DispatcherInterface as DispatcherInterface 18 | 19 | 20 | class Depend(Decorator): 21 | pre = True 22 | 23 | exec_target: ExecTarget 24 | cache: bool = False 25 | 26 | raw: Callable 27 | 28 | def __init__(self, callable: Callable, *, cache=False): 29 | self.cache = cache 30 | self.raw = callable 31 | 32 | if isgeneratorfunction(callable) or isgeneratorfunction(getattr(callable, "__call__", None)): 33 | callable = contextmanager(callable) 34 | elif isasyncgenfunction(callable) or isasyncgenfunction(getattr(callable, "__call__", None)): 35 | callable = asynccontextmanager(callable) 36 | 37 | self.exec_target = ExecTarget(callable) 38 | 39 | async def target(self, interface: DecoratorInterface): 40 | cache: dict = interface.local_storage["_depend_cached_results"] 41 | if self.raw in cache: 42 | return cache[self.raw] 43 | 44 | result_tier1 = await interface.dispatcher_interface.broadcast.Executor( 45 | target=self.exec_target, 46 | dispatchers=interface.dispatcher_interface.dispatchers, 47 | depth=interface.dispatcher_interface.depth + 1, 48 | ) 49 | stack: AsyncExitStack = interface.local_storage["_depend_lifespan_manager"] 50 | 51 | if isinstance(result_tier1, ContextManager): 52 | result = stack.enter_context(result_tier1) 53 | cache[self.raw] = result 54 | elif isinstance(result_tier1, AsyncContextManager): 55 | result = await stack.enter_async_context(result_tier1) 56 | cache[self.raw] = result 57 | else: 58 | result = result_tier1 59 | if self.cache: 60 | cache[self.raw] = result 61 | 62 | return Force(result) 63 | 64 | 65 | class DependDispatcher(BaseDispatcher): 66 | async def beforeExecution(self, interface: DispatcherInterface): 67 | interface.local_storage["_depend_lifespan_manager"] = AsyncExitStack() 68 | interface.local_storage["_depend_cached_results"] = {} 69 | 70 | async def catch(self, interface: DispatcherInterface): 71 | return 72 | 73 | async def afterExecution( 74 | self, 75 | interface: DispatcherInterface, 76 | exception: Exception | None, 77 | tb: TracebackType | None, 78 | ): 79 | stack: AsyncExitStack = interface.local_storage["_depend_lifespan_manager"] 80 | await stack.__aexit__(type(exception) if exception is not None else None, exception, tb) 81 | -------------------------------------------------------------------------------- /src/graia/broadcast/interrupt/__init__.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from asyncio import Future, get_running_loop 3 | from typing import Any, Optional, Type, Union 4 | 5 | from .. import Broadcast 6 | from ..entities.event import Dispatchable 7 | from ..entities.exectarget import ExecTarget 8 | from ..entities.signatures import RemoveMe 9 | from ..exceptions import PropagationCancelled 10 | from ..priority import Priority 11 | from ..utilles import dispatcher_mixin_handler 12 | from .waiter import Waiter 13 | 14 | 15 | class InterruptControl: 16 | """即中断控制, 主要是用于监听器/其他地方进行对符合特定要求的事件的捕获, 并返回事件. 17 | 18 | Methods: 19 | coroutine wait(interrupt: Interrupt) -> Any: 该方法主要用于在当前执行处堵塞当前协程, 20 | 同时将一个一次性使用的监听器挂载, 只要获取到符合条件的事件, 该方法会通过你传入的 `Interrupt` 实例的方法 `trigger`, 21 | 获取处理得到的值并返回; 无论如何, 用于一次性监听使用的监听器总会被销毁. 22 | """ 23 | 24 | broadcast: Broadcast 25 | 26 | def __init__(self, broadcast: Broadcast) -> None: 27 | self.broadcast = broadcast 28 | 29 | async def wait( 30 | self, waiter: Waiter, priority: Optional[Union[int, Priority]] = None, timeout: Optional[float] = None, **kwargs 31 | ): 32 | """生成一一次性使用的监听器并将其挂载, 该监听器用于获取特定类型的事件, 并根据设定对事件进行过滤; 33 | 当获取到符合条件的对象时, 堵塞将被解除, 同时该方法返回从监听器得到的值. 34 | 35 | Args: 36 | waiter (Waiter): 等待器 37 | priority (Union[int, Priority]): 中断 inline 监听器的优先级, Defaults to 15. 38 | **kwargs: 都会直接传入 Broadcast.receiver. 39 | 40 | Returns: 41 | Any: 通常这个值由中断本身定义并返回. 42 | """ 43 | future = get_running_loop().create_future() 44 | 45 | listeners = set() 46 | for event_type in waiter.listening_events: 47 | listener_callable = self.leader_listener_generator(waiter, event_type, future) 48 | self.broadcast.receiver(event_type, priority=priority or waiter.priority, **kwargs)(listener_callable) 49 | listener = self.broadcast.getListener(listener_callable) 50 | listeners.add(listener) 51 | 52 | try: 53 | return await asyncio.wait_for(future, timeout) if timeout else await future 54 | finally: # 删除 Listener 55 | if not future.done(): 56 | for i in listeners: 57 | self.broadcast.removeListener(i) 58 | 59 | def leader_listener_generator(self, waiter: Waiter, event_type: Type[Any], future: Future): 60 | async def inside_listener(event: event_type): 61 | if future.done(): 62 | return RemoveMe 63 | 64 | result = await self.broadcast.Executor( 65 | target=ExecTarget( 66 | callable=waiter.detected_event, 67 | inline_dispatchers=waiter.using_dispatchers, 68 | decorators=waiter.using_decorators, 69 | ), 70 | dispatchers=dispatcher_mixin_handler(event.Dispatcher) if hasattr(event, "Dispatcher") else [], 71 | ) 72 | # at present, the state of `future` is absolutely unknown. 73 | if result is not None and not future.done(): 74 | future.set_result(result) 75 | if not waiter.block_propagation: 76 | return RemoveMe 77 | raise PropagationCancelled() 78 | 79 | return inside_listener 80 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/test/dispatch.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import pytest 4 | 5 | from graia.broadcast import Broadcast 6 | from graia.broadcast.entities.dispatcher import BaseDispatcher 7 | from graia.broadcast.entities.event import Dispatchable 8 | from graia.broadcast.entities.signatures import Force 9 | from graia.broadcast.exceptions import ExecutionStop, RequirementCrashed 10 | from graia.broadcast.interfaces.dispatcher import DispatcherInterface 11 | 12 | 13 | class RandomDispatcher(BaseDispatcher): 14 | def __init__(self, t: bool = False) -> None: 15 | self.second_exec = t 16 | 17 | async def catch(self, interface: DispatcherInterface): 18 | if interface.name == "p": 19 | return "P_dispatcher" 20 | elif interface.name == "f": 21 | if self.second_exec: 22 | return 23 | self.second_exec = True 24 | return Force(2) 25 | 26 | 27 | class CrashDispatcher(BaseDispatcher): 28 | @staticmethod 29 | async def catch(i: DispatcherInterface): 30 | i.crash() 31 | 32 | 33 | class TestEvent(Dispatchable): 34 | class Dispatcher(BaseDispatcher): 35 | @staticmethod 36 | async def catch(interface: "DispatcherInterface"): 37 | if interface.name == "ster": 38 | return "1" 39 | if interface.name == "p": 40 | return "P_event" 41 | 42 | 43 | @pytest.mark.asyncio 44 | async def test_lookup_directly(): 45 | bcc = Broadcast() 46 | 47 | dii = DispatcherInterface(bcc, []) 48 | 49 | assert await dii.lookup_by_directly(RandomDispatcher(), "p", None, None) == "P_dispatcher" 50 | assert await dii.lookup_by_directly(RandomDispatcher(), "f", None, None) == 2 51 | 52 | 53 | @pytest.mark.asyncio 54 | async def test_crash(): 55 | bcc = Broadcast() 56 | 57 | dii = DispatcherInterface(bcc, []) 58 | with pytest.raises(RequirementCrashed): 59 | await dii.lookup_by_directly(CrashDispatcher, "u", None, None) 60 | with pytest.raises(ExecutionStop): 61 | dii.stop() 62 | 63 | 64 | @pytest.mark.asyncio 65 | async def test_insert(): 66 | bcc = Broadcast() 67 | 68 | dii = DispatcherInterface(bcc, []) 69 | 70 | t_a = RandomDispatcher() 71 | t_b = RandomDispatcher() 72 | dii.inject_execution_raw(t_a, t_b) 73 | assert dii.dispatchers == [t_b, t_a] 74 | 75 | 76 | @pytest.mark.asyncio 77 | async def test_event_dispatch(): 78 | bcc = Broadcast() 79 | 80 | executed = [] 81 | 82 | @bcc.receiver(TestEvent) 83 | async def _1( 84 | ster, 85 | p, 86 | b: Broadcast, 87 | e: TestEvent, 88 | e1: Dispatchable, 89 | i: DispatcherInterface, 90 | i1: DispatcherInterface[TestEvent], 91 | ): 92 | assert ster == "1" 93 | assert p == "P_event" 94 | assert b is bcc 95 | assert e.__class__ is TestEvent 96 | assert e1.__class__ is TestEvent 97 | assert i.__class__ is DispatcherInterface 98 | assert i1.__class__ is DispatcherInterface 99 | executed.append(1) 100 | 101 | await bcc.postEvent(TestEvent()) 102 | 103 | assert len(executed) == 1 104 | 105 | 106 | @pytest.mark.asyncio 107 | async def test_dispatcher_catch(): 108 | bcc = Broadcast() 109 | 110 | executed = [] 111 | 112 | @bcc.receiver(TestEvent, dispatchers=[RandomDispatcher(), RandomDispatcher()]) 113 | async def _1(f, b: Broadcast, i: DispatcherInterface): 114 | assert f == 2 115 | assert b is bcc 116 | assert i.__class__ == DispatcherInterface 117 | executed.append(1) 118 | 119 | await bcc.postEvent(TestEvent()) 120 | await bcc.postEvent(TestEvent()) 121 | 122 | assert len(executed) == 2 123 | 124 | 125 | @pytest.mark.asyncio 126 | @pytest.mark.xfail # need further discussion 127 | async def test_dispatcher_priority(): 128 | bcc = Broadcast() 129 | 130 | executed = [] 131 | 132 | @bcc.receiver(TestEvent, dispatchers=[RandomDispatcher]) 133 | async def _2(ster, p): 134 | assert ster == "1" 135 | assert p == "P_dispatcher" 136 | executed.append(1) 137 | 138 | await bcc.postEvent(TestEvent()) 139 | 140 | assert len(executed) == 1 141 | -------------------------------------------------------------------------------- /src/test/deco.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Optional 3 | 4 | import pytest 5 | 6 | from graia.broadcast import Broadcast 7 | from graia.broadcast.builtin.decorators import Depend, OptionalParam 8 | from graia.broadcast.entities.decorator import Decorator 9 | from graia.broadcast.entities.dispatcher import BaseDispatcher 10 | from graia.broadcast.entities.event import Dispatchable 11 | from graia.broadcast.entities.signatures import Force 12 | from graia.broadcast.interfaces.decorator import DecoratorInterface 13 | from graia.broadcast.interfaces.dispatcher import DispatcherInterface 14 | 15 | 16 | class TestDispatcher(BaseDispatcher): 17 | @staticmethod 18 | async def catch(interface: DispatcherInterface): 19 | if interface.name == "p": 20 | return 1 21 | 22 | 23 | class TestEvent(Dispatchable): 24 | class Dispatcher(BaseDispatcher): 25 | @staticmethod 26 | async def catch(interface: "DispatcherInterface"): 27 | if interface.name == "ster": 28 | return "1" 29 | 30 | 31 | class AsInt(Decorator): 32 | async def target(self, interface: "DecoratorInterface"): 33 | return int(interface.return_value) + 1 34 | 35 | 36 | class Integer(Decorator): 37 | o: int 38 | 39 | pre = True 40 | 41 | def __init__(self) -> None: 42 | self.o = 0 43 | 44 | async def target(self, interface: "DecoratorInterface"): 45 | if interface.name == "int_name": 46 | return int.__name__ 47 | if interface.annotation is type: 48 | return int 49 | if interface.event.__class__ == TestEvent and interface.name == "te": 50 | return -1 51 | self.o += 1 52 | return self.o 53 | 54 | 55 | class IntWhenInt(Decorator): 56 | pre = True 57 | 58 | async def target(self, interface: "DecoratorInterface"): 59 | if interface.annotation is int: 60 | return 1 61 | 62 | 63 | @pytest.mark.asyncio 64 | async def test_optional_param(): 65 | bcc = Broadcast() 66 | 67 | executed = [] 68 | 69 | @bcc.receiver(TestEvent) 70 | async def _( 71 | string: Optional[str] = OptionalParam(IntWhenInt()), 72 | val: Optional[int] = OptionalParam(IntWhenInt()), 73 | integer: int = OptionalParam(IntWhenInt()), 74 | ): 75 | assert string is None 76 | assert val is 1 77 | assert integer is 1 78 | executed.append(1) 79 | 80 | @bcc.receiver(TestEvent) 81 | async def _( 82 | val: Optional[int] = OptionalParam(AsInt()), 83 | ): 84 | assert val is None 85 | executed.append(1) 86 | 87 | await bcc.postEvent(TestEvent()) 88 | 89 | assert len(executed) == 2 90 | 91 | 92 | def test_force_recurse(): 93 | assert Force(Force(Force(None))).target == Force(None).target == Force().target == None 94 | 95 | 96 | @pytest.mark.asyncio 97 | async def test_decorator(): 98 | bcc = Broadcast() 99 | deco = AsInt() 100 | executed = [] 101 | 102 | @bcc.receiver(TestEvent) 103 | async def _(ster=deco): 104 | assert ster == 2 105 | executed.append(1) 106 | 107 | await bcc.postEvent(TestEvent()) 108 | 109 | assert executed 110 | 111 | 112 | @pytest.mark.asyncio 113 | async def test_decorator_pre(): 114 | bcc = Broadcast() 115 | 116 | executed = [] 117 | 118 | deco = Integer() 119 | 120 | @bcc.receiver(TestEvent) 121 | async def _(ster=deco, te=deco, int_name=deco, typ: type = deco): 122 | assert te == -1 123 | assert int_name == int.__name__ 124 | assert typ == int 125 | assert ster == deco.o 126 | executed.append(1) 127 | 128 | for _ in range(5): 129 | await bcc.postEvent(TestEvent()) 130 | 131 | assert len(executed) == 5 132 | 133 | 134 | async def dep(ster: str, p: int): 135 | return f"{ster}+{p}" 136 | 137 | 138 | @pytest.mark.asyncio 139 | async def test_depend(): 140 | bcc = Broadcast() 141 | 142 | executed = [] 143 | 144 | @bcc.receiver(TestEvent, dispatchers=[TestDispatcher]) 145 | async def _(ster=Depend(dep)): 146 | assert ster == "1+1" 147 | executed.append(1) 148 | 149 | depend = Depend(dep, cache=True) 150 | 151 | @bcc.receiver(TestEvent, dispatchers=[TestDispatcher]) 152 | async def _(a=depend, b=depend): 153 | assert a == b == "1+1" 154 | executed.append(1) 155 | 156 | for _ in range(5): 157 | await bcc.postEvent(TestEvent()) 158 | 159 | assert len(executed) == 10 160 | -------------------------------------------------------------------------------- /src/graia/broadcast/interfaces/dispatcher.py: -------------------------------------------------------------------------------- 1 | from asyncio import Future 2 | from typing import ( 3 | TYPE_CHECKING, 4 | Any, 5 | ClassVar, 6 | Dict, 7 | Generic, 8 | List, 9 | Set, 10 | Tuple, 11 | TypeVar, 12 | Union, 13 | ) 14 | 15 | from ..entities.signatures import Force 16 | from ..exceptions import ExecutionStop, RequirementCrashed 17 | from ..typing import T_Dispatcher 18 | from ..utilles import Ctx, NestableIterable 19 | 20 | try: 21 | from typing_extensions import get_args, get_origin 22 | except ImportError: 23 | from typing import get_args, get_origin 24 | 25 | try: 26 | from typing_extensions import Annotated 27 | except ImportError: 28 | from typing import Annotated 29 | 30 | if TYPE_CHECKING: 31 | from .. import Broadcast 32 | 33 | 34 | T_Event = TypeVar("T_Event", covariant=True) 35 | 36 | 37 | class DispatcherInterface(Generic[T_Event]): 38 | __slots__ = { 39 | "broadcast", 40 | "dispatchers", 41 | "parameter_contexts", 42 | "local_storage", 43 | "current_path", 44 | "current_oplog", 45 | "success", 46 | "_depth", 47 | "exec_result", 48 | } 49 | 50 | ctx: "ClassVar[Ctx[DispatcherInterface]]" = Ctx("bcc_dii") 51 | exec_result: "Future[Any]" 52 | 53 | broadcast: "Broadcast" 54 | dispatchers: List[T_Dispatcher] 55 | local_storage: Dict[str, Any] 56 | current_path: NestableIterable[T_Dispatcher] 57 | current_oplog: List[T_Dispatcher] 58 | 59 | parameter_contexts: List[Tuple[str, Any, Any]] 60 | success: Set[str] 61 | _depth: int 62 | 63 | def __init__(self, broadcast_instance: "Broadcast", dispatchers: List[T_Dispatcher], depth: int = 0) -> None: 64 | self.broadcast = broadcast_instance 65 | self.dispatchers = dispatchers 66 | self.parameter_contexts = [] 67 | self.local_storage = {} 68 | self.current_path = NestableIterable([]) 69 | self.current_oplog = [] 70 | self.success = set() 71 | self.exec_result = Future() 72 | self._depth = depth 73 | 74 | @property 75 | def name(self) -> str: 76 | return self.parameter_contexts[-1][0] 77 | 78 | @property 79 | def annotation(self) -> Any: 80 | return self.parameter_contexts[-1][1] 81 | 82 | @property 83 | def default(self) -> Any: 84 | return self.parameter_contexts[-1][2] 85 | 86 | @property 87 | def event(self) -> T_Event: 88 | return self.broadcast.event_ctx.get() # type: ignore 89 | 90 | @property 91 | def is_optional(self) -> bool: 92 | anno = self.annotation 93 | return get_origin(anno) is Union and type(None) in get_args(anno) 94 | 95 | @property 96 | def is_annotated(self) -> bool: 97 | return get_origin(self.annotation) is Annotated 98 | 99 | @property 100 | def annotated_origin(self) -> Any: 101 | if not self.is_annotated: 102 | raise TypeError("required a annotated annotation") 103 | return get_args(self.annotation)[0] 104 | 105 | @property 106 | def annotated_metadata(self) -> tuple: 107 | if not self.is_annotated: 108 | raise TypeError("required a annotated annotation") 109 | return get_args(self.annotation)[1:] 110 | 111 | @property 112 | def depth(self) -> int: 113 | return self._depth 114 | 115 | def inject_execution_raw(self, *dispatchers: T_Dispatcher): 116 | for dispatcher in dispatchers: 117 | self.dispatchers.insert(0, dispatcher) 118 | 119 | def crash(self): 120 | raise RequirementCrashed( 121 | self.name, 122 | self.annotation, 123 | self.default, 124 | ) 125 | 126 | def stop(self): 127 | raise ExecutionStop 128 | 129 | async def lookup_param( 130 | self, 131 | name: str, 132 | annotation: Any, 133 | default: Any, 134 | ) -> Any: 135 | self.parameter_contexts.append((name, annotation, default)) 136 | oplog = self.current_oplog 137 | 138 | try: 139 | if oplog: 140 | self.current_path.iterable = oplog 141 | for dispatcher in self.current_path: 142 | result = await dispatcher.catch(self) # type: ignore 143 | if result is None: # 不可靠. 144 | break 145 | self.success.add(name) 146 | if result.__class__ is Force: 147 | return result.target 148 | return result 149 | oplog.clear() 150 | self.current_path.iterable = self.dispatchers 151 | for dispatcher in self.current_path: 152 | result = await dispatcher.catch(self) # type: ignore 153 | 154 | if result is None: 155 | continue 156 | 157 | oplog.insert(0, dispatcher) 158 | 159 | if result.__class__ is Force: 160 | return result.target 161 | 162 | return result 163 | raise RequirementCrashed( 164 | self.name, 165 | self.annotation, 166 | self.default, 167 | ) 168 | finally: 169 | self.parameter_contexts.pop() 170 | 171 | async def lookup_by_directly(self, dispatcher: T_Dispatcher, name: str, annotation: Any, default: Any) -> Any: 172 | self.parameter_contexts.append((name, annotation, default)) 173 | 174 | try: 175 | result = await dispatcher.catch(self) # type: ignore 176 | if result.__class__ is Force: 177 | return result.target 178 | 179 | return result 180 | finally: 181 | self.parameter_contexts.pop() 182 | -------------------------------------------------------------------------------- /src/graia/broadcast/utilles.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import inspect 3 | from collections import UserList 4 | from contextlib import contextmanager 5 | from contextvars import ContextVar, Token 6 | from functools import lru_cache 7 | from types import TracebackType 8 | from typing import ( 9 | TYPE_CHECKING, 10 | Any, 11 | Callable, 12 | Dict, 13 | Generic, 14 | Iterable, 15 | List, 16 | Mapping, 17 | Optional, 18 | Type, 19 | TypeVar, 20 | Union, 21 | ) 22 | 23 | from .entities.dispatcher import BaseDispatcher 24 | 25 | if TYPE_CHECKING: 26 | from graia.broadcast.entities.event import Dispatchable 27 | from graia.broadcast.interfaces.dispatcher import DispatcherInterface 28 | from graia.broadcast.typing import T_Dispatcher 29 | 30 | 31 | async def run_always_await(callable, *args, **kwargs): 32 | obj = callable(*args, **kwargs) 33 | while inspect.isawaitable(obj): 34 | obj = await obj 35 | return obj 36 | 37 | 38 | T = TypeVar("T") 39 | D = TypeVar("D", type(None), Any) 40 | 41 | 42 | class Ctx(Generic[T]): 43 | current_ctx: ContextVar[T] 44 | 45 | def __init__(self, name: str) -> None: 46 | self.current_ctx = ContextVar(name) 47 | 48 | def get(self, default: Optional[Union[T, D]] = None) -> Union[T, D]: 49 | return self.current_ctx.get(default) 50 | 51 | def set(self, value: T): 52 | return self.current_ctx.set(value) 53 | 54 | def reset(self, token: Token): 55 | return self.current_ctx.reset(token) 56 | 57 | @contextmanager 58 | def use(self, value: T): 59 | token = self.set(value) 60 | yield 61 | self.reset(token) 62 | 63 | 64 | def printer(value: Any): 65 | print(value) 66 | return value 67 | 68 | 69 | class DebugList(UserList): 70 | def extend(self, item) -> None: 71 | print(item) 72 | return super().extend(item) 73 | 74 | def append(self, item) -> None: 75 | print(item) 76 | return super().append(item) 77 | 78 | def insert(self, i: int, item) -> None: 79 | print(i, item) 80 | return super().insert(i, item) 81 | 82 | 83 | K = TypeVar("K") 84 | 85 | 86 | def group_dict(iterable: Iterable[T], key_callable: Callable[[T], K]) -> Dict[K, List[T]]: 87 | temp = {} 88 | for i in iterable: 89 | k = key_callable(i) 90 | temp.setdefault(k, []) 91 | temp[k].append(i) 92 | return temp 93 | 94 | 95 | cache_size = 4096 96 | 97 | 98 | @lru_cache(cache_size) 99 | def argument_signature(callable_target: Callable): 100 | callable_annotation = get_annotations(callable_target, eval_str=True) 101 | return [ 102 | ( 103 | name, 104 | ( 105 | (callable_annotation.get(name) if isinstance(param.annotation, str) else param.annotation) 106 | if param.annotation is not inspect.Signature.empty 107 | else None 108 | ), 109 | param.default if param.default is not inspect.Signature.empty else None, 110 | ) 111 | for name, param in inspect.signature(callable_target).parameters.items() 112 | ] 113 | 114 | 115 | @lru_cache(cache_size) 116 | def is_asyncgener(o): 117 | return inspect.isasyncgenfunction(o) 118 | 119 | 120 | @lru_cache(cache_size) 121 | def iscoroutinefunction(o): 122 | return inspect.iscoroutinefunction(o) 123 | 124 | 125 | @lru_cache(cache_size) 126 | def isasyncgen(o): 127 | return inspect.isasyncgen(o) 128 | 129 | 130 | @lru_cache(None) 131 | def dispatcher_mixin_handler(dispatcher: Union[Type[BaseDispatcher], BaseDispatcher]) -> "List[T_Dispatcher]": 132 | unbound_mixin = getattr(dispatcher, "mixin", []) 133 | result: "List[T_Dispatcher]" = [dispatcher] 134 | 135 | for i in unbound_mixin: 136 | if issubclass(i, BaseDispatcher): 137 | result.extend(dispatcher_mixin_handler(i)) 138 | else: 139 | result.append(i) 140 | return result 141 | 142 | 143 | class NestableIterable(Iterable[T]): 144 | index_stack: list 145 | iterable: List[T] 146 | 147 | def __init__(self, iterable: List[T]) -> None: 148 | self.iterable = iterable 149 | self.index_stack = [-1] 150 | 151 | def __iter__(self): 152 | stack = self.index_stack 153 | index = stack[-1] 154 | stack.append(index) 155 | 156 | start_offset = index + 1 157 | try: 158 | for content in self.iterable[start_offset:]: 159 | stack[-1] += 1 160 | yield content 161 | finally: 162 | stack.pop() 163 | 164 | 165 | class _CoveredObjectMeta(type): 166 | if TYPE_CHECKING: 167 | __origin__: Any 168 | 169 | def __instancecheck__(self, __instance: Any) -> bool: 170 | return isinstance(__instance, self.__origin__.__class__) 171 | 172 | 173 | class CoveredObject(metaclass=_CoveredObjectMeta): 174 | def __init__(self, obj: Any, cover_params: Dict[str, Any]): 175 | for k, v in cover_params.items(): 176 | if k.startswith("__") and k.endswith("__"): 177 | raise TypeError("you should not cover any magic method.") 178 | setattr(self, k, v) 179 | self.__origin__ = obj 180 | self.__covered__ = cover_params 181 | 182 | def __getattribute__(self, key: str): 183 | if key in {"__origin__", "__covered__"}: 184 | return super().__getattribute__(key) 185 | covered = super().__getattribute__("__covered__") 186 | if key in covered: 187 | return covered[key] 188 | origin = super().__getattribute__("__origin__") 189 | return getattr(origin, key) 190 | 191 | def __call__(self, *args, **kwargs): 192 | origin = super().__getattribute__("__origin__") 193 | return origin(*args, **kwargs) 194 | 195 | 196 | class CoverDispatcher(BaseDispatcher): 197 | origin: "T_Dispatcher" 198 | event: "Dispatchable" 199 | 200 | def __init__(self, origin: "T_Dispatcher", event: "Dispatchable") -> None: 201 | self.origin = origin 202 | self.event = event 203 | 204 | async def beforeExecution(self, interface: "DispatcherInterface"): 205 | if self.origin.beforeExecution: 206 | return await self.origin.beforeExecution(CoveredObject(interface, {"event": self.event})) # type: ignore 207 | 208 | async def catch(self, interface: "DispatcherInterface"): 209 | return await self.origin.catch(CoveredObject(interface, {"event": self.event})) # type: ignore 210 | 211 | async def afterDispatch( 212 | self, interface: "DispatcherInterface", exception: Optional[Exception], tb: Optional[TracebackType] 213 | ): 214 | if self.origin.afterDispatch: 215 | return await self.origin.afterDispatch(CoveredObject(interface, {"event": self.event}), exception, tb) # type: ignore 216 | 217 | async def afterExecution( 218 | self, interface: "DispatcherInterface", exception: Optional[Exception], tb: Optional[TracebackType] 219 | ): 220 | if self.origin.afterExecution: 221 | return await self.origin.afterExecution(CoveredObject(interface, {"event": self.event}), exception, tb) # type: ignore 222 | 223 | 224 | try: 225 | from inspect import get_annotations # type: ignore 226 | except ImportError: 227 | 228 | def get_annotations( 229 | obj: Callable, 230 | *, 231 | globals: Optional[Mapping[str, Any]] = None, 232 | locals: Optional[Mapping[str, Any]] = None, 233 | eval_str: bool = False, 234 | ) -> Dict[str, Any]: # sourcery skip: avoid-builtin-shadow 235 | if not callable(obj): 236 | raise TypeError(f"{obj!r} is not a module, class, or callable.") 237 | 238 | ann = getattr(obj, "__annotations__", None) 239 | obj_globals = getattr(obj, "__globals__", None) 240 | obj_locals = None 241 | unwrap = obj 242 | if ann is None: 243 | return {} 244 | 245 | if not isinstance(ann, dict): 246 | raise ValueError(f"{unwrap!r}.__annotations__ is neither a dict nor None") 247 | if not ann: 248 | return {} 249 | 250 | if not eval_str: 251 | return dict(ann) 252 | 253 | if unwrap is not None: 254 | while True: 255 | if hasattr(unwrap, "__wrapped__"): 256 | unwrap = unwrap.__wrapped__ 257 | continue 258 | if isinstance(unwrap, functools.partial): 259 | unwrap = unwrap.func 260 | continue 261 | break 262 | if hasattr(unwrap, "__globals__"): 263 | obj_globals = unwrap.__globals__ 264 | 265 | if globals is None: 266 | globals = obj_globals 267 | if locals is None: 268 | locals = obj_locals 269 | 270 | return {key: eval(value, globals, locals) if isinstance(value, str) else value for key, value in ann.items()} # type: ignore 271 | -------------------------------------------------------------------------------- /src/graia/broadcast/__init__.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import inspect 3 | import pprint 4 | import sys 5 | import traceback 6 | from contextlib import asynccontextmanager 7 | from typing import ( 8 | Any, 9 | Callable, 10 | Dict, 11 | Iterable, 12 | List, 13 | Optional, 14 | Set, 15 | Type, 16 | Union, 17 | get_origin, 18 | ) 19 | 20 | from .builtin.defer import DeferDispatcher 21 | from .builtin.depend import DependDispatcher 22 | from .builtin.derive import DeriveDispatcher 23 | from .builtin.event import EventExceptionThrown 24 | from .entities.decorator import Decorator 25 | from .entities.dispatcher import BaseDispatcher 26 | from .entities.event import Dispatchable 27 | from .entities.exectarget import ExecTarget 28 | from .entities.listener import Listener 29 | from .entities.namespace import Namespace 30 | from .entities.signatures import Force, RemoveMe 31 | from .exceptions import ( 32 | DisabledNamespace, 33 | ExecutionStop, 34 | ExistedNamespace, 35 | InvalidEventName, 36 | PropagationCancelled, 37 | RegisteredEventListener, 38 | RequirementCrashed, 39 | UnexistedNamespace, 40 | ) 41 | from .interfaces.decorator import DecoratorInterface 42 | from .interfaces.dispatcher import DispatcherInterface 43 | from .typing import T_Dispatcher 44 | from .utilles import ( 45 | CoverDispatcher, 46 | Ctx, 47 | argument_signature, 48 | dispatcher_mixin_handler, 49 | group_dict, 50 | run_always_await, 51 | ) 52 | 53 | 54 | class Broadcast: 55 | default_namespace: Namespace 56 | namespaces: List[Namespace] 57 | listeners: List[Listener] 58 | 59 | decorator_interface: DecoratorInterface 60 | 61 | event_ctx: Ctx[Dispatchable] 62 | 63 | prelude_dispatchers: List["T_Dispatcher"] 64 | finale_dispatchers: List["T_Dispatcher"] 65 | 66 | _background_tasks: Set[asyncio.Task] = set() 67 | 68 | def __init__(self): 69 | self.default_namespace = Namespace(name="default", default=True) 70 | self.namespaces = [] 71 | self.listeners = [] 72 | self.event_ctx = Ctx("bcc_event_ctx") 73 | self.decorator_interface = DecoratorInterface() 74 | self.prelude_dispatchers = [self.decorator_interface, DependDispatcher(), DeriveDispatcher()] 75 | self.finale_dispatchers = [DeferDispatcher()] 76 | 77 | @self.prelude_dispatchers.append 78 | class BroadcastBuiltinDispatcher(BaseDispatcher): 79 | @classmethod 80 | async def catch(cls, interface: DispatcherInterface): 81 | annotation = get_origin(interface.annotation) or interface.annotation 82 | if annotation is Broadcast: 83 | return interface.broadcast 84 | if annotation is DispatcherInterface: 85 | return interface 86 | if annotation is interface.event.__class__: 87 | return interface.event 88 | if isinstance(annotation, type) and isinstance(interface.event, annotation): 89 | return interface.event 90 | 91 | def default_listener_generator(self, event_class) -> Iterable[Listener]: 92 | return list( 93 | filter( 94 | lambda x: all( 95 | [ 96 | not x.namespace.hide, 97 | not x.namespace.disabled, 98 | event_class in x.listening_events, 99 | ] 100 | ), 101 | self.listeners, 102 | ) 103 | ) 104 | 105 | async def layered_scheduler( 106 | self, 107 | listener_generator: Iterable[Listener], 108 | event: Any, 109 | addition_dispatchers: Optional[List["T_Dispatcher"]] = None, 110 | ): 111 | grouped: Dict[int, List[Listener]] = group_dict( 112 | listener_generator, lambda x: x.priorities.get(event.__class__) or x.priority 113 | ) 114 | event_dispatcher_mixin = [] 115 | if hasattr(event, "Dispatcher"): 116 | event_dispatcher_mixin = dispatcher_mixin_handler(event.Dispatcher) 117 | if addition_dispatchers: 118 | event_dispatcher_mixin = event_dispatcher_mixin + addition_dispatchers 119 | with self.event_ctx.use(event): 120 | for _, current_group in sorted(grouped.items(), key=lambda x: x[0]): 121 | tasks = [ 122 | asyncio.create_task(self.Executor(target=i, dispatchers=event_dispatcher_mixin)) 123 | for i in current_group 124 | ] 125 | done_tasks, _ = await asyncio.wait(tasks) 126 | for task in done_tasks: 127 | if task.exception().__class__ is PropagationCancelled: 128 | return 129 | 130 | async def Executor( 131 | self, 132 | target: Union[Callable, ExecTarget], 133 | dispatchers: Optional[List[T_Dispatcher]] = None, 134 | post_exception_event: bool = True, 135 | print_exception: bool = True, 136 | use_global_dispatchers: bool = True, 137 | depth: int = 0, 138 | ): 139 | is_exectarget = is_listener = False 140 | current_oplog = None 141 | if isinstance(target, Listener): 142 | is_exectarget = is_listener = True 143 | current_oplog = target.oplog.setdefault(self.event_ctx.get().__class__, {}) 144 | # if it's a listener, the event should be set. 145 | elif isinstance(target, ExecTarget): 146 | is_exectarget = True 147 | current_oplog = target.oplog.setdefault(..., {}) 148 | # also, Ellipsis is good. 149 | 150 | if is_listener and target.namespace.disabled: # type: ignore 151 | raise DisabledNamespace("caught a disabled namespace: {0}".format(target.namespace.name)) # type: ignore 152 | 153 | target_callable: Callable = target.callable if is_exectarget else target # type: ignore 154 | parameter_compile_result = {} 155 | 156 | dispatchers: List[T_Dispatcher] = [ 157 | *(self.prelude_dispatchers if use_global_dispatchers else []), 158 | *(dispatchers if dispatchers else []), 159 | *(target.dispatchers if is_exectarget else []), 160 | *(target.namespace.injected_dispatchers if is_listener else []), # type: ignore 161 | *(self.finale_dispatchers if use_global_dispatchers else []), 162 | ] 163 | 164 | dii = DispatcherInterface(self, dispatchers, depth) 165 | dii_token = dii.ctx.set(dii) 166 | try: 167 | for dispatcher in dispatchers: 168 | i = getattr(dispatcher, "beforeExecution", None) 169 | if i: 170 | await run_always_await(i, dii) # type: ignore 171 | 172 | if is_exectarget: 173 | for name, annotation, default in argument_signature(target_callable): 174 | origin = current_oplog.get(name) # type: ignore 175 | dii.current_oplog = origin.copy() if origin else [] 176 | parameter_compile_result[name] = await dii.lookup_param(name, annotation, default) 177 | if name not in dii.success: 178 | current_oplog[name] = dii.current_oplog # type: ignore 179 | 180 | dii.current_oplog = [] 181 | for hl_d in target.decorators: 182 | await dii.lookup_by_directly( 183 | self.decorator_interface, 184 | "_bcc_headless_decorators", 185 | None, 186 | hl_d, 187 | ) 188 | 189 | else: 190 | for name, annotation, default in argument_signature(target_callable): 191 | parameter_compile_result[name] = await dii.lookup_param(name, annotation, default) 192 | 193 | for dispatcher in dispatchers: 194 | i = getattr(dispatcher, "afterDispatch", None) 195 | if i: 196 | await run_always_await(i, dii, None, None) 197 | result = await run_always_await(target_callable, **parameter_compile_result) 198 | dii.exec_result.set_result(result) 199 | except (ExecutionStop, PropagationCancelled) as e: 200 | dii.exec_result.set_result(e) 201 | raise 202 | except RequirementCrashed as e: 203 | dii.exec_result.set_exception(e) 204 | if depth != 0: 205 | if not hasattr(e, "__target__"): 206 | e.__target__ = target_callable 207 | raise e 208 | name, *_ = e.args 209 | param = inspect.signature(getattr(e, "__target__", target_callable)).parameters[name] 210 | code = target_callable.__code__ 211 | etype: Type[Exception] = type( 212 | "RequirementCrashed", 213 | (RequirementCrashed, SyntaxError), 214 | {}, 215 | ) 216 | _args = (code.co_filename, code.co_firstlineno, 1, str(param)) 217 | if sys.version_info >= (3, 10): 218 | _args += (code.co_firstlineno, len(name) + 1) 219 | traceback.print_exception( 220 | etype, 221 | etype( 222 | f"Unable to lookup parameter ({param}) by dispatchers\n{pprint.pformat(dispatchers)}", 223 | _args, 224 | ), 225 | e.__traceback__, 226 | ) 227 | raise 228 | except Exception as e: 229 | dii.exec_result.set_exception(e) 230 | if depth != 0: 231 | raise 232 | event: Optional[Dispatchable] = self.event_ctx.get() 233 | if event is not None and event.__class__ is not EventExceptionThrown: 234 | if print_exception: 235 | traceback.print_exc() 236 | if post_exception_event: 237 | self.postEvent(EventExceptionThrown(exception=e, event=event)) 238 | raise 239 | finally: 240 | _, exception, tb = sys.exc_info() 241 | for dispatcher in dispatchers: 242 | i = getattr(dispatcher, "afterExecution", None) 243 | if i: 244 | await run_always_await(i, dii, exception, tb) # type: ignore 245 | 246 | dii.ctx.reset(dii_token) 247 | 248 | result = dii.exec_result.result() 249 | if result.__class__ is Force: 250 | return result.target 251 | elif result is RemoveMe: 252 | if is_listener and target in self.listeners: 253 | self.listeners.remove(target) 254 | return result 255 | 256 | @asynccontextmanager 257 | async def param_compile( 258 | self, 259 | dispatchers: Optional[List[T_Dispatcher]] = None, 260 | post_exception_event: bool = True, 261 | print_exception: bool = True, 262 | use_global_dispatchers: bool = True, 263 | ): 264 | dispatchers: List[T_Dispatcher] = [ 265 | *(self.prelude_dispatchers if use_global_dispatchers else []), 266 | *(dispatchers if dispatchers else []), 267 | *(self.finale_dispatchers if use_global_dispatchers else []), 268 | ] 269 | 270 | dii = DispatcherInterface(self, dispatchers, 0) 271 | dii_token = dii.ctx.set(dii) 272 | 273 | try: 274 | for dispatcher in dispatchers: 275 | i = getattr(dispatcher, "beforeExecution", None) 276 | if i: 277 | await run_always_await(i, dii) # type: ignore 278 | yield dii 279 | except RequirementCrashed: 280 | traceback.print_exc() 281 | raise 282 | except Exception as e: 283 | event: Optional[Dispatchable] = self.event_ctx.get() 284 | if event is not None and event.__class__ is not EventExceptionThrown: 285 | if print_exception: 286 | traceback.print_exc() 287 | if post_exception_event: 288 | self.postEvent(EventExceptionThrown(exception=e, event=event)) 289 | raise 290 | finally: 291 | _, exception, tb = sys.exc_info() 292 | for dispatcher in dispatchers: 293 | i = getattr(dispatcher, "afterExecution", None) 294 | if i: 295 | await run_always_await(i, dii, exception, tb) # type: ignore 296 | 297 | dii.ctx.reset(dii_token) 298 | 299 | def postEvent(self, event: Any, upper_event: Optional[Any] = None): 300 | if not hasattr(self, "_loop"): 301 | from creart import it 302 | 303 | self._loop = it(asyncio.AbstractEventLoop) 304 | task = self._loop.create_task( 305 | self.layered_scheduler( 306 | listener_generator=self.default_listener_generator(event.__class__), 307 | event=event, 308 | addition_dispatchers=( 309 | [CoverDispatcher(i, upper_event) for i in dispatcher_mixin_handler(upper_event.Dispatcher)] 310 | if upper_event and hasattr(upper_event, "Dispatcher") 311 | else [] 312 | ), 313 | ) 314 | ) 315 | self._background_tasks.add(task) 316 | task.add_done_callback(self._background_tasks.discard) 317 | return task 318 | 319 | @staticmethod 320 | def event_class_generator(target=Dispatchable): 321 | for i in target.__subclasses__(): 322 | yield i 323 | if i.__subclasses__(): 324 | yield from Broadcast.event_class_generator(i) 325 | 326 | @staticmethod 327 | def findEvent(name: str): 328 | for i in Broadcast.event_class_generator(): 329 | if i.__name__ == name: 330 | return i 331 | 332 | def getDefaultNamespace(self): 333 | return self.default_namespace 334 | 335 | def createNamespace(self, name, *, priority: int = 0, hide: bool = False, disabled: bool = False): 336 | if self.containNamespace(name): 337 | raise ExistedNamespace(name, "has been created!") 338 | self.namespaces.append(Namespace(name=name, priority=priority, hide=hide, disabled=disabled)) 339 | return self.namespaces[-1] 340 | 341 | def removeNamespace(self, name): 342 | if not self.containNamespace(name): 343 | raise UnexistedNamespace(name) 344 | for index, i in enumerate(self.namespaces): 345 | if i.name == name: 346 | self.namespaces.pop(index) 347 | return 348 | 349 | def containNamespace(self, name): 350 | return any(i.name == name for i in self.namespaces) 351 | 352 | def getNamespace(self, name) -> "Namespace": 353 | if self.containNamespace(name): 354 | for i in self.namespaces: 355 | if i.name == name: 356 | return i 357 | raise UnexistedNamespace(name) 358 | 359 | def hideNamespace(self, name): 360 | ns = self.getNamespace(name) 361 | ns.hide = True 362 | 363 | def unhideNamespace(self, name): 364 | ns = self.getNamespace(name) 365 | ns.hide = False 366 | 367 | def disableNamespace(self, name): 368 | ns = self.getNamespace(name) 369 | ns.disabled = True 370 | 371 | def enableNamespace(self, name): 372 | ns = self.getNamespace(name) 373 | ns.disabled = False 374 | 375 | def containListener(self, target): 376 | return any(i.callable == target for i in self.listeners) 377 | 378 | def getListener(self, target): 379 | for i in self.listeners: 380 | if i.callable == target: 381 | return i 382 | 383 | def removeListener(self, target): 384 | self.listeners.remove(target) 385 | 386 | def receiver( 387 | self, 388 | event: Union[str, Type[Any]], 389 | priority: int = 16, 390 | dispatchers: Optional[List[T_Dispatcher]] = None, 391 | namespace: Optional[Namespace] = None, 392 | decorators: Optional[List[Decorator]] = None, 393 | ): 394 | if isinstance(event, str): 395 | _name = event 396 | event = self.findEvent(event) # type: ignore 397 | if not event: 398 | raise InvalidEventName(f"{_name} is not valid!") 399 | priority = int(priority) 400 | 401 | def receiver_wrapper(callable_target): 402 | listener = self.getListener(callable_target) 403 | if not listener: 404 | self.listeners.append( 405 | Listener( 406 | callable=callable_target, 407 | namespace=namespace or self.getDefaultNamespace(), 408 | inline_dispatchers=dispatchers or [], 409 | priority=priority, 410 | listening_events=[event], # type: ignore 411 | decorators=decorators or [], 412 | ) 413 | ) 414 | elif event in listener.listening_events: 415 | raise RegisteredEventListener(event.__name__, "has been registered!") # type: ignore 416 | else: 417 | listener.listening_events.append(event) # type: ignore 418 | return callable_target 419 | 420 | return receiver_wrapper 421 | -------------------------------------------------------------------------------- /pdm.lock: -------------------------------------------------------------------------------- 1 | # This file is @generated by PDM. 2 | # It is not intended for manual editing. 3 | 4 | [metadata] 5 | groups = ["default", "dev"] 6 | cross_platform = true 7 | static_urls = false 8 | lock_version = "4.3" 9 | content_hash = "sha256:6f8f84ce30f8f8737da9b2d3968be8228b885e7ed39c2ab73234fb066261bf10" 10 | 11 | [[package]] 12 | name = "attrs" 13 | version = "22.1.0" 14 | requires_python = ">=3.5" 15 | summary = "Classes Without Boilerplate" 16 | files = [ 17 | {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, 18 | {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, 19 | ] 20 | 21 | [[package]] 22 | name = "black" 23 | version = "22.10.0" 24 | requires_python = ">=3.7" 25 | summary = "The uncompromising code formatter." 26 | dependencies = [ 27 | "click>=8.0.0", 28 | "mypy-extensions>=0.4.3", 29 | "pathspec>=0.9.0", 30 | "platformdirs>=2", 31 | "tomli>=1.1.0; python_full_version < \"3.11.0a7\"", 32 | "typing-extensions>=3.10.0.0; python_version < \"3.10\"", 33 | ] 34 | files = [ 35 | {file = "black-22.10.0-1fixedarch-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:5cc42ca67989e9c3cf859e84c2bf014f6633db63d1cbdf8fdb666dcd9e77e3fa"}, 36 | {file = "black-22.10.0-1fixedarch-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:5d8f74030e67087b219b032aa33a919fae8806d49c867846bfacde57f43972ef"}, 37 | {file = "black-22.10.0-1fixedarch-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:197df8509263b0b8614e1df1756b1dd41be6738eed2ba9e9769f3880c2b9d7b6"}, 38 | {file = "black-22.10.0-1fixedarch-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:2644b5d63633702bc2c5f3754b1b475378fbbfb481f62319388235d0cd104c2d"}, 39 | {file = "black-22.10.0-1fixedarch-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:e41a86c6c650bcecc6633ee3180d80a025db041a8e2398dcc059b3afa8382cd4"}, 40 | {file = "black-22.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2039230db3c6c639bd84efe3292ec7b06e9214a2992cd9beb293d639c6402edb"}, 41 | {file = "black-22.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ff67aec0a47c424bc99b71005202045dc09270da44a27848d534600ac64fc7"}, 42 | {file = "black-22.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:819dc789f4498ecc91438a7de64427c73b45035e2e3680c92e18795a839ebb66"}, 43 | {file = "black-22.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5b9b29da4f564ba8787c119f37d174f2b69cdfdf9015b7d8c5c16121ddc054ae"}, 44 | {file = "black-22.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b49776299fece66bffaafe357d929ca9451450f5466e997a7285ab0fe28e3b"}, 45 | {file = "black-22.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:21199526696b8f09c3997e2b4db8d0b108d801a348414264d2eb8eb2532e540d"}, 46 | {file = "black-22.10.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e464456d24e23d11fced2bc8c47ef66d471f845c7b7a42f3bd77bf3d1789650"}, 47 | {file = "black-22.10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:9311e99228ae10023300ecac05be5a296f60d2fd10fff31cf5c1fa4ca4b1988d"}, 48 | {file = "black-22.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fba8a281e570adafb79f7755ac8721b6cf1bbf691186a287e990c7929c7692ff"}, 49 | {file = "black-22.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:915ace4ff03fdfff953962fa672d44be269deb2eaf88499a0f8805221bc68c87"}, 50 | {file = "black-22.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:444ebfb4e441254e87bad00c661fe32df9969b2bf224373a448d8aca2132b395"}, 51 | {file = "black-22.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:974308c58d057a651d182208a484ce80a26dac0caef2895836a92dd6ebd725e0"}, 52 | {file = "black-22.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72ef3925f30e12a184889aac03d77d031056860ccae8a1e519f6cbb742736383"}, 53 | {file = "black-22.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:432247333090c8c5366e69627ccb363bc58514ae3e63f7fc75c54b1ea80fa7de"}, 54 | {file = "black-22.10.0-py3-none-any.whl", hash = "sha256:c957b2b4ea88587b46cf49d1dc17681c1e672864fd7af32fc1e9664d572b3458"}, 55 | {file = "black-22.10.0.tar.gz", hash = "sha256:f513588da599943e0cde4e32cc9879e825d58720d6557062d1098c5ad80080e1"}, 56 | ] 57 | 58 | [[package]] 59 | name = "cfgv" 60 | version = "3.3.1" 61 | requires_python = ">=3.6.1" 62 | summary = "Validate configuration and produce human readable error messages." 63 | files = [ 64 | {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, 65 | {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, 66 | ] 67 | 68 | [[package]] 69 | name = "click" 70 | version = "8.1.3" 71 | requires_python = ">=3.7" 72 | summary = "Composable command line interface toolkit" 73 | dependencies = [ 74 | "colorama; platform_system == \"Windows\"", 75 | ] 76 | files = [ 77 | {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, 78 | {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, 79 | ] 80 | 81 | [[package]] 82 | name = "colorama" 83 | version = "0.4.5" 84 | requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 85 | summary = "Cross-platform colored terminal text." 86 | files = [ 87 | {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, 88 | {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, 89 | ] 90 | 91 | [[package]] 92 | name = "coverage" 93 | version = "6.5.0" 94 | requires_python = ">=3.7" 95 | summary = "Code coverage measurement for Python" 96 | files = [ 97 | {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"}, 98 | {file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"}, 99 | {file = "coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4"}, 100 | {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04"}, 101 | {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0"}, 102 | {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae"}, 103 | {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466"}, 104 | {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a"}, 105 | {file = "coverage-6.5.0-cp310-cp310-win32.whl", hash = "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32"}, 106 | {file = "coverage-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e"}, 107 | {file = "coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795"}, 108 | {file = "coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75"}, 109 | {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b"}, 110 | {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91"}, 111 | {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4"}, 112 | {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa"}, 113 | {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b"}, 114 | {file = "coverage-6.5.0-cp311-cp311-win32.whl", hash = "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578"}, 115 | {file = "coverage-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b"}, 116 | {file = "coverage-6.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d"}, 117 | {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3"}, 118 | {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef"}, 119 | {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79"}, 120 | {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d"}, 121 | {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c"}, 122 | {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f"}, 123 | {file = "coverage-6.5.0-cp37-cp37m-win32.whl", hash = "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b"}, 124 | {file = "coverage-6.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2"}, 125 | {file = "coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c"}, 126 | {file = "coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba"}, 127 | {file = "coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e"}, 128 | {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398"}, 129 | {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b"}, 130 | {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b"}, 131 | {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f"}, 132 | {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e"}, 133 | {file = "coverage-6.5.0-cp38-cp38-win32.whl", hash = "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d"}, 134 | {file = "coverage-6.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6"}, 135 | {file = "coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745"}, 136 | {file = "coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc"}, 137 | {file = "coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe"}, 138 | {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf"}, 139 | {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5"}, 140 | {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62"}, 141 | {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518"}, 142 | {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f"}, 143 | {file = "coverage-6.5.0-cp39-cp39-win32.whl", hash = "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72"}, 144 | {file = "coverage-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"}, 145 | {file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"}, 146 | {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"}, 147 | ] 148 | 149 | [[package]] 150 | name = "creart" 151 | version = "0.3.0" 152 | requires_python = ">=3.8" 153 | summary = "a universal, extensible class instantiation helper" 154 | dependencies = [ 155 | "importlib-metadata>=3.6", 156 | ] 157 | files = [ 158 | {file = "creart-0.3.0-py3-none-any.whl", hash = "sha256:43074f6f59430f41b72d3c04ba4d268af0f32842fbc94bbda4b81ae464be0ee1"}, 159 | {file = "creart-0.3.0.tar.gz", hash = "sha256:39fea77476d26d2bd5891aa3b5f16cab5567b37b855483e37f094ba005bf0d1f"}, 160 | ] 161 | 162 | [[package]] 163 | name = "distlib" 164 | version = "0.3.6" 165 | summary = "Distribution utilities" 166 | files = [ 167 | {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, 168 | {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, 169 | ] 170 | 171 | [[package]] 172 | name = "filelock" 173 | version = "3.8.0" 174 | requires_python = ">=3.7" 175 | summary = "A platform independent file lock." 176 | files = [ 177 | {file = "filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"}, 178 | {file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"}, 179 | ] 180 | 181 | [[package]] 182 | name = "flake8" 183 | version = "4.0.1" 184 | requires_python = ">=3.6" 185 | summary = "the modular source code checker: pep8 pyflakes and co" 186 | dependencies = [ 187 | "mccabe<0.7.0,>=0.6.0", 188 | "pycodestyle<2.9.0,>=2.8.0", 189 | "pyflakes<2.5.0,>=2.4.0", 190 | ] 191 | files = [ 192 | {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, 193 | {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, 194 | ] 195 | 196 | [[package]] 197 | name = "identify" 198 | version = "2.5.6" 199 | requires_python = ">=3.7" 200 | summary = "File identification library for Python" 201 | files = [ 202 | {file = "identify-2.5.6-py2.py3-none-any.whl", hash = "sha256:b276db7ec52d7e89f5bc4653380e33054ddc803d25875952ad90b0f012cbcdaa"}, 203 | {file = "identify-2.5.6.tar.gz", hash = "sha256:6c32dbd747aa4ceee1df33f25fed0b0f6e0d65721b15bd151307ff7056d50245"}, 204 | ] 205 | 206 | [[package]] 207 | name = "importlib-metadata" 208 | version = "4.2.0" 209 | requires_python = ">=3.6" 210 | summary = "Read metadata from Python packages" 211 | dependencies = [ 212 | "zipp>=0.5", 213 | ] 214 | files = [ 215 | {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, 216 | {file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"}, 217 | ] 218 | 219 | [[package]] 220 | name = "iniconfig" 221 | version = "1.1.1" 222 | summary = "iniconfig: brain-dead simple config-ini parsing" 223 | files = [ 224 | {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, 225 | {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, 226 | ] 227 | 228 | [[package]] 229 | name = "isort" 230 | version = "5.10.1" 231 | requires_python = ">=3.6.1,<4.0" 232 | summary = "A Python utility / library to sort Python imports." 233 | files = [ 234 | {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, 235 | {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, 236 | ] 237 | 238 | [[package]] 239 | name = "mccabe" 240 | version = "0.6.1" 241 | summary = "McCabe checker, plugin for flake8" 242 | files = [ 243 | {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, 244 | {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, 245 | ] 246 | 247 | [[package]] 248 | name = "mypy-extensions" 249 | version = "0.4.3" 250 | summary = "Experimental type system extensions for programs checked with the mypy typechecker." 251 | files = [ 252 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 253 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 254 | ] 255 | 256 | [[package]] 257 | name = "nodeenv" 258 | version = "1.7.0" 259 | requires_python = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" 260 | summary = "Node.js virtual environment builder" 261 | dependencies = [ 262 | "setuptools", 263 | ] 264 | files = [ 265 | {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, 266 | {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, 267 | ] 268 | 269 | [[package]] 270 | name = "packaging" 271 | version = "21.3" 272 | requires_python = ">=3.6" 273 | summary = "Core utilities for Python packages" 274 | dependencies = [ 275 | "pyparsing!=3.0.5,>=2.0.2", 276 | ] 277 | files = [ 278 | {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, 279 | {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, 280 | ] 281 | 282 | [[package]] 283 | name = "pathspec" 284 | version = "0.10.1" 285 | requires_python = ">=3.7" 286 | summary = "Utility library for gitignore style pattern matching of file paths." 287 | files = [ 288 | {file = "pathspec-0.10.1-py3-none-any.whl", hash = "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93"}, 289 | {file = "pathspec-0.10.1.tar.gz", hash = "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d"}, 290 | ] 291 | 292 | [[package]] 293 | name = "platformdirs" 294 | version = "2.5.2" 295 | requires_python = ">=3.7" 296 | summary = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 297 | files = [ 298 | {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, 299 | {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, 300 | ] 301 | 302 | [[package]] 303 | name = "pluggy" 304 | version = "1.0.0" 305 | requires_python = ">=3.6" 306 | summary = "plugin and hook calling mechanisms for python" 307 | files = [ 308 | {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, 309 | {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, 310 | ] 311 | 312 | [[package]] 313 | name = "pre-commit" 314 | version = "2.20.0" 315 | requires_python = ">=3.7" 316 | summary = "A framework for managing and maintaining multi-language pre-commit hooks." 317 | dependencies = [ 318 | "cfgv>=2.0.0", 319 | "identify>=1.0.0", 320 | "nodeenv>=0.11.1", 321 | "pyyaml>=5.1", 322 | "toml", 323 | "virtualenv>=20.0.8", 324 | ] 325 | files = [ 326 | {file = "pre_commit-2.20.0-py2.py3-none-any.whl", hash = "sha256:51a5ba7c480ae8072ecdb6933df22d2f812dc897d5fe848778116129a681aac7"}, 327 | {file = "pre_commit-2.20.0.tar.gz", hash = "sha256:a978dac7bc9ec0bcee55c18a277d553b0f419d259dadb4b9418ff2d00eb43959"}, 328 | ] 329 | 330 | [[package]] 331 | name = "py" 332 | version = "1.11.0" 333 | requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 334 | summary = "library with cross-python path, ini-parsing, io, code, log facilities" 335 | files = [ 336 | {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, 337 | {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, 338 | ] 339 | 340 | [[package]] 341 | name = "pycodestyle" 342 | version = "2.8.0" 343 | requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 344 | summary = "Python style guide checker" 345 | files = [ 346 | {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, 347 | {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, 348 | ] 349 | 350 | [[package]] 351 | name = "pyflakes" 352 | version = "2.4.0" 353 | requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 354 | summary = "passive checker of Python programs" 355 | files = [ 356 | {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, 357 | {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, 358 | ] 359 | 360 | [[package]] 361 | name = "pyparsing" 362 | version = "3.0.9" 363 | requires_python = ">=3.6.8" 364 | summary = "pyparsing module - Classes and methods to define and execute parsing grammars" 365 | files = [ 366 | {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, 367 | {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, 368 | ] 369 | 370 | [[package]] 371 | name = "pytest" 372 | version = "7.1.3" 373 | requires_python = ">=3.7" 374 | summary = "pytest: simple powerful testing with Python" 375 | dependencies = [ 376 | "attrs>=19.2.0", 377 | "colorama; sys_platform == \"win32\"", 378 | "iniconfig", 379 | "packaging", 380 | "pluggy<2.0,>=0.12", 381 | "py>=1.8.2", 382 | "tomli>=1.0.0", 383 | ] 384 | files = [ 385 | {file = "pytest-7.1.3-py3-none-any.whl", hash = "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7"}, 386 | {file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"}, 387 | ] 388 | 389 | [[package]] 390 | name = "pytest-asyncio" 391 | version = "0.19.0" 392 | requires_python = ">=3.7" 393 | summary = "Pytest support for asyncio" 394 | dependencies = [ 395 | "pytest>=6.1.0", 396 | ] 397 | files = [ 398 | {file = "pytest-asyncio-0.19.0.tar.gz", hash = "sha256:ac4ebf3b6207259750bc32f4c1d8fcd7e79739edbc67ad0c58dd150b1d072fed"}, 399 | {file = "pytest_asyncio-0.19.0-py3-none-any.whl", hash = "sha256:7a97e37cfe1ed296e2e84941384bdd37c376453912d397ed39293e0916f521fa"}, 400 | ] 401 | 402 | [[package]] 403 | name = "pyyaml" 404 | version = "6.0" 405 | requires_python = ">=3.6" 406 | summary = "YAML parser and emitter for Python" 407 | files = [ 408 | {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, 409 | {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, 410 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, 411 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, 412 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, 413 | {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, 414 | {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, 415 | {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, 416 | {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, 417 | {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, 418 | {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, 419 | {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, 420 | {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, 421 | {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, 422 | {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, 423 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, 424 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, 425 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, 426 | {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, 427 | {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, 428 | {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, 429 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, 430 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, 431 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, 432 | {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, 433 | {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, 434 | {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, 435 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, 436 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, 437 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, 438 | {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, 439 | {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, 440 | {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, 441 | {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, 442 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, 443 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, 444 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, 445 | {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, 446 | {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, 447 | {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, 448 | ] 449 | 450 | [[package]] 451 | name = "setuptools" 452 | version = "65.5.0" 453 | requires_python = ">=3.7" 454 | summary = "Easily download, build, install, upgrade, and uninstall Python packages" 455 | files = [ 456 | {file = "setuptools-65.5.0-py3-none-any.whl", hash = "sha256:f62ea9da9ed6289bfe868cd6845968a2c854d1427f8548d52cae02a42b4f0356"}, 457 | {file = "setuptools-65.5.0.tar.gz", hash = "sha256:512e5536220e38146176efb833d4a62aa726b7bbff82cfbc8ba9eaa3996e0b17"}, 458 | ] 459 | 460 | [[package]] 461 | name = "toml" 462 | version = "0.10.2" 463 | requires_python = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 464 | summary = "Python Library for Tom's Obvious, Minimal Language" 465 | files = [ 466 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 467 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 468 | ] 469 | 470 | [[package]] 471 | name = "tomli" 472 | version = "2.0.1" 473 | requires_python = ">=3.7" 474 | summary = "A lil' TOML parser" 475 | files = [ 476 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 477 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 478 | ] 479 | 480 | [[package]] 481 | name = "typing-extensions" 482 | version = "4.4.0" 483 | requires_python = ">=3.7" 484 | summary = "Backported and Experimental Type Hints for Python 3.7+" 485 | files = [ 486 | {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, 487 | {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, 488 | ] 489 | 490 | [[package]] 491 | name = "virtualenv" 492 | version = "20.16.2" 493 | requires_python = ">=3.6" 494 | summary = "Virtual Python Environment builder" 495 | dependencies = [ 496 | "distlib<1,>=0.3.1", 497 | "filelock<4,>=3.2", 498 | "platformdirs<3,>=2", 499 | ] 500 | files = [ 501 | {file = "virtualenv-20.16.2-py2.py3-none-any.whl", hash = "sha256:635b272a8e2f77cb051946f46c60a54ace3cb5e25568228bd6b57fc70eca9ff3"}, 502 | {file = "virtualenv-20.16.2.tar.gz", hash = "sha256:0ef5be6d07181946891f5abc8047fda8bc2f0b4b9bf222c64e6e8963baee76db"}, 503 | ] 504 | 505 | [[package]] 506 | name = "zipp" 507 | version = "3.9.0" 508 | requires_python = ">=3.7" 509 | summary = "Backport of pathlib-compatible object wrapper for zip files" 510 | files = [ 511 | {file = "zipp-3.9.0-py3-none-any.whl", hash = "sha256:972cfa31bc2fedd3fa838a51e9bc7e64b7fb725a8c00e7431554311f180e9980"}, 512 | {file = "zipp-3.9.0.tar.gz", hash = "sha256:3a7af91c3db40ec72dd9d154ae18e008c69efe8ca88dde4f9a731bb82fe2f9eb"}, 513 | ] 514 | --------------------------------------------------------------------------------