diff options
| -rw-r--r-- | mkdocs.yml | 224 | ||||
| -rw-r--r-- | netlify.toml | 4 | ||||
| -rw-r--r-- | src/infini/__init__.py | 28 | ||||
| -rw-r--r-- | src/infini/const.py | 3 | ||||
| -rw-r--r-- | src/infini/core.py | 39 | ||||
| -rw-r--r-- | src/infini/event.py | 98 | ||||
| -rw-r--r-- | src/infini/exceptions.py | 43 | ||||
| -rw-r--r-- | src/infini/generator.py | 2 | ||||
| -rw-r--r-- | src/infini/handler.py | 62 | ||||
| -rw-r--r-- | src/infini/input.py | 21 | ||||
| -rw-r--r-- | src/infini/interceptor.py | 35 | ||||
| -rw-r--r-- | src/infini/internal.py | 18 | ||||
| -rw-r--r-- | src/infini/logging.py | 33 | ||||
| -rw-r--r-- | src/infini/matcher.py | 34 | ||||
| -rw-r--r-- | src/infini/output.py | 19 | ||||
| -rw-r--r-- | src/infini/queue.py | 47 | ||||
| -rw-r--r-- | src/infini/register.py | 163 | ||||
| -rw-r--r-- | src/infini/router.py | 21 | ||||
| -rw-r--r-- | src/infini/settings.py | 1 | ||||
| -rw-r--r-- | src/infini/typing.py | 28 | ||||
| -rw-r--r-- | src/infini/utils/cli.py | 28 | ||||
| -rw-r--r-- | src/infini/utils/gui.py | 5 | ||||
| -rw-r--r-- | tests/test_handlers.py | 84 | ||||
| -rw-r--r-- | tests/test_input.py | 16 | ||||
| -rw-r--r-- | tests/test_interceptor.py | 33 |
25 files changed, 381 insertions, 708 deletions
diff --git a/mkdocs.yml b/mkdocs.yml deleted file mode 100644 index e7b7ba6f..00000000 --- a/mkdocs.yml +++ /dev/null @@ -1,224 +0,0 @@ -site_name: Infini 核心文档 -repo_url: https://github.com/HydroRoll-Team/infini -repo_name: HydroRoll-Team/infini -edit_uri: edit/master/docs/ -site_author: HsiangNianian -site_description: >- - 一个基于通用规则包标准的规则包加载器 - -theme: - name: material - language: zh - favicon: images/logo.png - icon: - logo: material/book-sync - repo: fontawesome/brands/git-alt - annotation: material/arrow-right-circle - custom_dir: docs/src/overrides - palette: - - scheme: default - primary: indigo - accent: indigo - toggle: - icon: material/brightness-7 - name: Switch to dark mode - - scheme: slate - primary: black - accent: indigo - toggle: - icon: material/brightness-4 - name: Switch to light mode - features: - - announce.dismiss - - content.action.edit - - content.action.view - - content.tooltips - - content.tabs.link - - content.code.annotate - - content.code.copy - - content.code.select - - navigation.footer - - navigation.tabs - # - navigation.tabs.sticky - - navigation.sections - # - navigation.expand - - navigation.path - - navigation.indexes - - toc.follow - - navigation.top - - search.highlight - - search.suggest - - search.share - - header.autohide - - navigation.footer - - navigation.instant - - navigation.instant.prefetch - - navigation.instant.progress - - font: - text: Times New - code: Times New -extra: - status: - new: 最近添加 - deprecated: 已被废弃 - version: - provider: mike - homepage: https://grps.hydroroll.team # hydroroll-team.github.io - consent: - title: Cookie 偏好设置 - description: >- - 我们使用Cookie来识别您的重复访问和偏好,以及衡量我们的文档的有效性和用户是否找到他们搜索的内容。在您的同意下,这将帮助我们改善我们的文档。 - actions: - - accept - - manage - cookies: - analytics: - name: Google Analytics - checked: true - github: - name: GitHub - checked: true - - social: - - icon: fontawesome/brands/github - link: https://github.com/HydroRoll-Team - - icon: fontawesome/brands/python - link: https://pypi.org/project/infini/ - -copyright: > - Copyright © 2023 - PRESENT. <a href="#__consent">(管理Cookies)</a> - -markdown_extensions: - - attr_list - - md_in_html - - pymdownx.superfences - - pymdownx.snippets: - auto_append: - - RulePackage/getStart/getDeeper/Module.md - - abbr - - admonition - - def_list - - footnotes - - toc: - permalink: ⚓ - - pymdownx.arithmatex: - generic: true - - pymdownx.betterem: - smart_enable: all - - pymdownx.caret - - pymdownx.details - - pymdownx.emoji: - emoji_index: !!python/name:material.extensions.emoji.twemoji - emoji_generator: !!python/name:material.extensions.emoji.to_svg - - pymdownx.highlight: - anchor_linenums: true - line_spans: __span - pygments_lang_class: true - linenums: true - - pymdownx.inlinehilite - - pymdownx.keys - - pymdownx.magiclink: - normalize_issue_symbols: true - repo_url_shorthand: true - user: HydroRoll-Team - repo: infini - - pymdownx.mark - - pymdownx.smartsymbols - - pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format - - pymdownx.tabbed: - alternate_style: true - combine_header_slug: true - slugify: !!python/object/apply:pymdownx.slugs.slugify - kwds: - case: lower - - pymdownx.tasklist: - custom_checkbox: true - - pymdownx.tilde - -plugins: - - blog - - search: - separator: '[\s\u200b\-_,:!=\[\]()"`/]+|\.(?!\d)|&[lg]t;|(?!\b)(?=[A-Z][a-z])' - - git-authors - - mike: - version_selector: true - - mkdocstrings: - handlers: - python: - options: - docstring_style: google - - git-revision-date-localized: - enable_creation_date: true - fallback_to_build_date: true - timezone: Asia/Shanghai - type: timeago - custom_format: "%d. %B %Y" - - git-committers: - repository: HydroRoll-Team/infini - - minify: - minify_html: true -nav: - - 主页: index.md - - 水系核心: - - "核心概览": Core/index.md - - 规则包入门: - - 概述: RulePackage/intro.md - - 开始: - - "类": RulePackage/getStart/class.md - - "方法": RulePackage/getStart/method.md - - "属性": RulePackage/getStart/attribute.md - - 深入: - - "对象": RulePackage/getStart/getDeeper/object.md - - "模块": RulePackage/getStart/getDeeper/Module.md - - "包": RulePackage/getStart/getDeeper/Package.md - - 实战: - - "了解Python": RulePackage/Write/python.md - - "准备规则书": RulePackage/Write/prepare.md - - "编写规则包": RulePackage/Write/write.md - - 技巧: - - RulePackage/skill/index.md - - "善用语法糖": RulePackage/skill/fstring.md - - 通用规则包标准: - - 什么是规则包?: Standard/what-is-rule-package.md - - 为什么要做规则包?: Standard/why-rule-package.md - - 什么是通用规则包标准?: Standard/what-is-standard.md - - 术语表(草案): - - setting 背景: Standard/Term/setting.md - - scenario 剧本: Standard/Term/scenario.md - - duration 时长: - - Standard/Term/duration/index.md - - sene 场: Standard/Term/duration/sene.md - - module 模组: Standard/Term/duration/module.md - - campaign 战役: Standard/Term/duration/campaign.md - - DefaultDice 默认骰子: - - Standard/Term/DefaultDice/index.md - - _sides 骰子面数: Standard/Term/DefaultDice/_sides.md - - _counts 骰子个数: Standard/Term/DefaultDice/_counts.md - - check 检定: - - Standard/Term/check/index.md - - ability 检定: Standard/Term/check/ability.md - - PlayerCard 人物卡: - - Standard/Term/PlayerCard/index.md - - CLI 参考: - - "CLI Reference": CLI/index.md - - API 参考: - - "API Reference": API/index.md - - 其他帮助: - - "术语表一览": Help/TermList.md - - "Python 技巧": Help/PythonSkill.md - - "图标、图例、注释说明": Help/emoji.md - - 博客: - - blog/index.md - - 日志: - - changelog.md - -extra_css: - - stylesheets/extra.css - -extra_javascript: - - "https://samuelcolvin.github.io/mkdocs-run-code/run_code_main.js" diff --git a/netlify.toml b/netlify.toml deleted file mode 100644 index 37b79714..00000000 --- a/netlify.toml +++ /dev/null @@ -1,4 +0,0 @@ -# netlify.toml -[build] - command = "mkdocs build" - publish = "site" diff --git a/src/infini/__init__.py b/src/infini/__init__.py index af9b8fa1..e69de29b 100644 --- a/src/infini/__init__.py +++ b/src/infini/__init__.py @@ -1,28 +0,0 @@ -"""Infini - -Core of HydroRoll, python version of GRPS-1. - - -本模块从子模块导入了以下内容: -- `Handler` => [`infini.handler`](./handler#Handler) -- `MessageEvent` => [`infini.event.MessageEvent`](./event#MessageEvent) -- `MatcherEvent` => [`infini.event.MatcherEvent`](./event#MatcherEvent) -- `WorkflowEvent` => [`infini.event.WorkflowEvent`](./event#WorkflowEvent) -- `Matcher` => [`infini.matcher.Matcher`](./matcher/#Matcher) -- `Register` => [`infini.register.Register`](./register#Register) -""" - - -from infini.handler import Handler -from infini.event import MessageEvent, MatcherEvent, WorkflowEvent -from infini.matcher import Matcher -from infini.register import Register - -__all__ = [ - "Handler", - "MessageEvent", - "Matcher", - "Register", - "MatcherEvent", - "WorkflowEvent", -] diff --git a/src/infini/const.py b/src/infini/const.py new file mode 100644 index 00000000..2ec42488 --- /dev/null +++ b/src/infini/const.py @@ -0,0 +1,3 @@ +from pathlib import Path + +SRC_HOME = Path.home() / ".ipm" / "src"
\ No newline at end of file diff --git a/src/infini/core.py b/src/infini/core.py new file mode 100644 index 00000000..2bb3e7da --- /dev/null +++ b/src/infini/core.py @@ -0,0 +1,39 @@ +from infini.input import Input +from infini.interceptor import Interceptor +from infini.generator import Generator +from infini.handler import Handler +from infini.output import Output + + +class Core: + pre_interceptor: Interceptor + handler: Handler + generator: Generator + interceptor: Interceptor + + def input(self, input: Input): + if isinstance(pre_intercepted_stream := self.pre_intercept(input), Output): + yield self.generate(pre_intercepted_stream) + return + for handled_stream in self.handle(pre_intercepted_stream): + if handled_stream.is_empty(): + return # TODO 处理未找到 Handler + yield self.intercept(self.generate(handled_stream)) + + def pre_intercept(self, input: Input) -> Input | Output: + return self.pre_interceptor.input(input) + + def handle(self, input: Input): + iterator = self.handler.input(input) + for output in iterator: + yield output + + def generate(self, output: Output) -> str: + return output.name # TODO 生成器实现 + + def intercept(self, output: str) -> str: + return ( + self.generate(callback) + if isinstance(callback := self.interceptor.output(output), Output) + else callback + ) diff --git a/src/infini/event.py b/src/infini/event.py deleted file mode 100644 index 5ef9444e..00000000 --- a/src/infini/event.py +++ /dev/null @@ -1,98 +0,0 @@ -"""Infini 事件模块 - -此模块定义了 Infini 的事件类,包括 InfiniEvent, MessageEvent, WorkflowEvent 和 MatcherEvent。 -这些事件类用于在 Infini 框架中处理各种事件。 -""" - -from abc import ABCMeta, abstractmethod -from .typing import Dict, Any - -__all__ = [ - "InfiniEvent", - "MessageEvent", - "WorkflowEvent", - "MatcherEvent", -] - - -class InfiniEvent(metaclass=ABCMeta): - """Infini 事件基类""" - - name: str - kwargs: Dict[str, Any] - - @abstractmethod - def __repr__(self) -> str: - raise NotImplementedError - - def get_event_name(self) -> str: - return self.name - - -class MessageEvent(InfiniEvent): - """Message 事件""" - - name: str - output: str - kwargs: Dict[str, Any] - - def __init__(self, name: str, **kwargs) -> None: - self.name = name - self.kwargs = kwargs - - def __str__(self) -> str: - return f"<MessageEvent [{self.name}]>" - - def __repr__(self) -> str: - return self.__str__() - - -class WorkflowEvent(InfiniEvent): - """Workflow 事件""" - - name: str - kwargs: Dict[str, Any] - - def __init__(self, name: str, **kwargs) -> None: - self.name = name - self.kwargs = kwargs - - def __repr__(self) -> str: - return f"<WorkflowEvent [{self.name}]>" - - def __eq__(self, __value: object) -> bool: - if __value is str: - return self.name == __value - if isinstance(__value, WorkflowEvent): - return self.name == __value.name and self.kwargs == __value.kwargs - return False - - -class MatcherEvent(InfiniEvent): - """Matcher 事件""" - - name: str - prefix: str - string: str - kwargs: Dict[str, Any] - - def __init__( - self, - event_name: str, - prefix: str | None = None, - string: str | None = None, - **kwargs, - ): - self.name = event_name - self.prefix = prefix or "" - self.string = string or "" - self.kwargs = kwargs - - def __repr__(self) -> str: - return f"<MatcherEvent [{self.name}]>" - - def get_prefix(self): - return self.prefix - - def get_plain_text(self): - return self.string diff --git a/src/infini/exceptions.py b/src/infini/exceptions.py index 2038efcd..cb0a9e41 100644 --- a/src/infini/exceptions.py +++ b/src/infini/exceptions.py @@ -1,45 +1,6 @@ -"""Infini 异常处理模块 - -此模块定义了 Infini 项目中所有的自定义异常类。 -规则包作者后续实现的每个异常类都应该继承自 InfiniException。 -""" - - class InfiniException(Exception): """Infini 异常基类""" -class RulePackageException(InfiniException): - """由规则包抛出的异常基类, 所有规则包抛出的异常都应该继承此类。""" - - -class LoadError(InfiniException): - """加载规则包错误, 找不到指定的规则包或事件声明有误导致运行时错误时抛出。""" - - -class PackageNotFound(LoadError): - """规则包不存在时错误""" - - -class EventLoadError(LoadError, RuntimeError): - """事件声明导入失败""" - - -class HandlerLoadError(LoadError, RuntimeError): - """业务函数导入失败""" - - -class EventException(InfiniException): - """事件异常基类""" - - -class UnknownMatcherEvent(EventException): - """未知的给入实现""" - - -class UnknownMessageEvent(EventException): - """未知的给出实现""" - - -class UnsupportedError(EventException, RuntimeError): - """方法未被支持""" +class KeyError(InfiniException): + """键值错误""" diff --git a/src/infini/generator.py b/src/infini/generator.py new file mode 100644 index 00000000..a4d60e27 --- /dev/null +++ b/src/infini/generator.py @@ -0,0 +1,2 @@ +class Generator: + ... diff --git a/src/infini/handler.py b/src/infini/handler.py index 0f72eccf..ae3561f8 100644 --- a/src/infini/handler.py +++ b/src/infini/handler.py @@ -1,34 +1,28 @@ -"""Infini Handler - -此类是所有规则包业务类的基类。 -每个规则包业务类都需要定义一个名为 process 的抽象方法, 用于接收和处理传入的 MatcherEvent 事件,并返回一个 InfiniEvent 事件。 -此外,每个规则包业务类还可以定义一个名为 priority 的类属性,用于指定该业务类的优先级。优先级越高,该业务类处理事件的顺序越靠前。 -""" - -from abc import ABC, abstractmethod -from enum import Enum -from .typing import StateT, ClassVar, Generic -from .event import MatcherEvent, InfiniEvent - -__all__ = ["Handler", "HandlerLoadType"] - - -class HandlerLoadType(Enum): - """规则包加载类型。""" - - DIR = "dir" - NAME = "name" - FILE = "file" - CLASS = "class" - - -class Handler(ABC, Generic[StateT]): - """规则包业务基类""" - - name: str - priority: ClassVar[int] = 0 - block: ClassVar[bool] = False - - @abstractmethod - def process(self, event: MatcherEvent) -> InfiniEvent: - raise NotImplementedError +from infini.input import Input +from infini.output import Output +from infini.typing import List, RouterType, Callable +from infini.queue import EventQueue + + +class Handler: + handlers: List[RouterType] + + def input(self, input: Input): + queue = self.match(input.get_plain_text()) + if queue.is_empty(): + yield Output.empty() + return + while not queue.is_empty(): + output = queue.pop()(input) + yield output + if output.block: + return + + def match(self, text: str) -> EventQueue[Callable[[Input], Output]]: + queue = EventQueue() + + for handler in self.handlers: + if handler["router"].match(text): + queue.push(handler["priority"], handler["handler"]) + + return queue diff --git a/src/infini/input.py b/src/infini/input.py new file mode 100644 index 00000000..21c12a2f --- /dev/null +++ b/src/infini/input.py @@ -0,0 +1,21 @@ +from infini.typing import Dict, Any, Generic, T + + +class Input(Generic[T]): + plain_data: T + variables: Dict[str, Any] + + def __init__(self, plain_data: Any, variables: Dict[str, Any] | None = None) -> None: + self.plain_data = plain_data + self.variables = variables or {} + + def get_session_id(self) -> str: + if session_id := self.variables.get("session_id"): + return session_id + + user_id = self.variables.get("user_id", "unknown") + group_id = self.variables.get("group_id", "unknown") + return f"session_{group_id}_{user_id}" + + def get_plain_text(self) -> str: + return str(self.plain_data) diff --git a/src/infini/interceptor.py b/src/infini/interceptor.py new file mode 100644 index 00000000..54387281 --- /dev/null +++ b/src/infini/interceptor.py @@ -0,0 +1,35 @@ +from infini.input import Input +from infini.output import Output +from infini.typing import List, RouterType, Callable, Generic, T, overload +from infini.queue import EventQueue + + +class Interceptor: + interceptors: List[RouterType] + + def input(self, input: Input) -> Input | Output: + queue = self.match(input.get_plain_text()) + while not queue.is_empty(): + if isinstance(intercepted := queue.pop()(input), Output): + return intercepted # TODO 允许拦截器产出文本 + else: + input = intercepted + return input + + def output(self, output_text: str) -> str | Output: + queue = self.match(output_text) + while not queue.is_empty(): + if isinstance(intercepted := queue.pop()(input), Output): + return intercepted + else: + input = intercepted + return output_text + + def match(self, text: str) -> EventQueue[Callable[[Input], Input | Output]]: + queue = EventQueue() + + for interceptor in self.interceptors: + if interceptor["router"].match(text): + queue.push(interceptor["priority"], interceptor["handler"]) + + return queue diff --git a/src/infini/internal.py b/src/infini/internal.py new file mode 100644 index 00000000..4f800297 --- /dev/null +++ b/src/infini/internal.py @@ -0,0 +1,18 @@ +from infini.typing import List, ModuleType +from infini.const import SRC_HOME + +import importlib +import sys + + +def require(name: str, paths: List | None = None) -> ModuleType: + paths = [ + str(path) + for path in ( + (list(paths) + [str(SRC_HOME / name)]) if paths else [str(SRC_HOME / name)] + ) + ] + sys.path.extend(paths) + module = importlib.import_module(name) + (sys.path.remove(path) for path in paths) + return module diff --git a/src/infini/logging.py b/src/infini/logging.py deleted file mode 100644 index 979fee7d..00000000 --- a/src/infini/logging.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Infini 日志。 - -Infini 使用 [loguru](https://github.com/Delgan/loguru) 来记录日志信息。 -自定义 logger 请参考 [loguru](https://github.com/Delgan/loguru) 文档。 -""" -from datetime import datetime -from multilogging import multilogger -from pathlib import Path -from .settings import DEBUG - - -__all__ = ["logger", "error_or_exception"] - -logger = multilogger(name="Infini", payload="Core", - level="DEBUG" if DEBUG else "INFO") -CURRENT_PATH = Path(__file__).resolve().parent -DATA_PATH = Path.home() / ".infini" -LOG_PATH = DATA_PATH / "logs" -if not LOG_PATH.exists(): - LOG_PATH.mkdir(parents=True, exist_ok=True) -logger.add( - sink=LOG_PATH / (datetime.now().strftime("%Y-%m-%d") + ".log"), - level="INFO", - rotation="10 MB", -) # 每个日志文件最大为 10MB - - -def error_or_exception(message: str, exception: Exception, verbose: bool = True): - if verbose: - logger.exception(exception) - logger.critical(message) - else: - logger.critical(f"{message} {exception!r}") diff --git a/src/infini/matcher.py b/src/infini/matcher.py deleted file mode 100644 index 8c11ff29..00000000 --- a/src/infini/matcher.py +++ /dev/null @@ -1,34 +0,0 @@ -"""Infini Matcher - -用于处理和匹配事件。 -""" - -from .register import Register, register -from .event import MatcherEvent -from .handler import Handler -from .exceptions import UnknownMatcherEvent - - -class Matcher: - """事件处理单元""" - - register: Register - - def __init__(self, _register: Register | None = None) -> None: - self.register = _register or register - - def match(self, name: str) -> Handler: - if handler := self.register.handlers.match(name): - return handler if isinstance(handler, Handler) else handler() - else: - raise UnknownMatcherEvent(f"未知的规则包: {name}") - - def run(self, event: MatcherEvent) -> str: - callback_event = self.match(event.name).process(event) - return self.register.events.process( - callback_event.name, - **callback_event.kwargs or callback_event.kwargs, - ) - - -matcher = Matcher() diff --git a/src/infini/output.py b/src/infini/output.py new file mode 100644 index 00000000..ddb7f0a5 --- /dev/null +++ b/src/infini/output.py @@ -0,0 +1,19 @@ +from infini.typing import Literal + + +class Output: + type: Literal["null", "text", "workflow"] + name: str + status: int + block: bool + + @classmethod + def empty(cls) -> "Output": + output = cls() + output.type = "null" + output.status = 0 + output.block = True + return output + + def is_empty(self) -> bool: + return self.type == "null" diff --git a/src/infini/queue.py b/src/infini/queue.py new file mode 100644 index 00000000..9dd441ee --- /dev/null +++ b/src/infini/queue.py @@ -0,0 +1,47 @@ +from infini.exceptions import KeyError +from infini.typing import Generic, T +from collections import deque + +import heapq + + +class EventQueue(Generic[T]): + heap: list + entry_finder: dict + counter: int + order_queue: deque + + def __init__(self) -> None: + self.heap = [] + self.entry_finder = {} + self.counter = 0 + self.order_queue = deque() + + def push(self, priority: int, item: T) -> None: + if item in self.entry_finder: + self.remove(item) + entry = [priority, self.counter, item] + self.counter += 1 + self.entry_finder[item] = entry + heapq.heappush(self.heap, entry) + self.order_queue.appendleft(item) + + def pop(self) -> T: + while self.heap: + _, _, item = heapq.heappop(self.heap) + if self.entry_finder[item] is not None: + del self.entry_finder[item] + self.order_queue.remove(item) + return item + raise KeyError("事件队列为空.") + + def remove(self, item) -> None: + entry = self.entry_finder.pop(item) + entry[-1] = None + + def is_empty(self) -> bool: + return not bool(self.heap) + + def generate(self): + while not self.is_empty(): + yield self.pop() diff --git a/src/infini/register.py b/src/infini/register.py deleted file mode 100644 index 55677152..00000000 --- a/src/infini/register.py +++ /dev/null @@ -1,163 +0,0 @@ -"""Infini 注册模块 - -包含了 Infini 事件和处理器的注册和加载功能。 -""" - -from pathlib import Path -from .exceptions import UnknownMatcherEvent, UnsupportedError -from .handler import Handler -from .event import InfiniEvent, MessageEvent, WorkflowEvent -from .typing import Dict, Type -from .exceptions import LoadError, EventLoadError, HandlerLoadError - -import re -import sys -import importlib -import inspect - - -class Loader: - """Infini 事件加载器 - - Raises: - LoadError: 规则包导入错误 - EventLoadError: 规则包事件导入错误 - HandlerLoadError: 规则包业务函数导入错误 - """ - - name: str - meta_path: Path - events: dict - handlers: dict - - def __init__(self, meta_path: Path | str) -> None: - if isinstance(meta_path, str): - self.meta_path = Path(meta_path).resolve() - else: - self.meta_path = meta_path.resolve() - self.name = self.meta_path.name - self.events = {} - self.handlers = {} - - def load(self) -> None: - sys.path.append(str(self.meta_path)) - try: - importlib.import_module(f"{self.name}") - except Exception as error: - raise LoadError(f"规则包[{self.name}]导入失败: {error}") from error - - try: - event_module = importlib.import_module(f"{self.name}.event") - events = [ - cls[1] - for cls in inspect.getmembers(event_module, inspect.isclass) - if issubclass(cls[1], InfiniEvent) - and not cls[1].__module__.startswith("infini") - ] - for event in events: - self.events[f"{self.name}.{event.__dict__['name']}"] = event - except Exception as error: - raise EventLoadError(f"规则包[{self.name}]事件导入失败: {error}") from error - - try: - handler_module = importlib.import_module(f"{self.name}.handler") - handlers = [ - cls[1] - for cls in inspect.getmembers(handler_module, inspect.isclass) - if issubclass(cls[1], Handler) - and not cls[1].__module__.startswith("infini") - ] - for handler in handlers: - self.handlers[f"{self.name}.{handler.__dict__['name']}"] = handler - except Exception as error: - raise HandlerLoadError(f"规则包[{self.name}]业务函数导入失败: {error}") from error - - sys.path.remove(str(self.meta_path)) - - -class Events: - """规则包事件集合 - - Raises: - UnsupportedError: 事件尚未被支持 - UnknownMatcherEvent: 事件不存在 - - Returns: - _type_: _description_ - """ - - _events: Dict[str, Type[InfiniEvent]] - - def __init__(self) -> None: - self._events = {} - - def register(self, name: str, event: Type[InfiniEvent]) -> None: - self._events[name.lower()] = event - - def update(self, _events: dict) -> None: - self._events.update(_events) - - def _process(self, event: Type[InfiniEvent], **kwargs) -> str: - # sourcery skip: merge-duplicate-blocks - if issubclass(event, MessageEvent): - return self._format(event.__dict__["output"], **kwargs) - elif issubclass(event, WorkflowEvent): - # TODO: handle workflow events - raise UnsupportedError - else: - raise UnsupportedError - - def process(self, name: str, **kwargs) -> str: - if event := self._events.get(name.lower()): - return self._process(event, **kwargs) - raise UnknownMatcherEvent(f"事件[{name.lower()}]不存在!") - - def _format(self, string: str, **kwargs): - pattern = r"{(.*?)}" - values = re.findall(pattern, string) - for value in values: - kwarg = kwargs.get(value) - value = kwarg or "" - string = re.sub(pattern, value, string) - return string - - -class Handlers: - """规则包业务集合 - - Returns: - _type_: _description_ - """ - - _handlers: Dict[str, Handler] = {} - - def register(self, name: str, handler: Handler) -> None: - self._handlers[name.lower()] = handler - - def update(self, _events: dict) -> None: - self._handlers.update(_events) - - def match(self, name: str) -> Handler | None: - return self._handlers.get(name.lower()) - - -class Register: - """注册器""" - - events: Events - handlers: Handlers - - def __init__(self) -> None: - self.events = Events() - self.handlers = Handlers() - - def register( - self, meta_path: Path | str | None = None, loader: Type[Loader] | None = None - ): - _loader = Loader(meta_path or ".") if not loader else loader(meta_path or ".") - _loader.load() - self.events.update(_loader.events) - self.handlers.update(_loader.handlers) - - -register = Register() diff --git a/src/infini/router.py b/src/infini/router.py new file mode 100644 index 00000000..a520a456 --- /dev/null +++ b/src/infini/router.py @@ -0,0 +1,21 @@ +from infini.typing import overload + + +class Router: + sign: str + + def __init__(self, sign: str) -> None: + self.sign = sign + + def match(self, input: str) -> bool: + return self.sign == input.strip() + + +class StartswithRouter(Router): + def match(self, input: str) -> bool: + return input.strip().startswith(self.sign) + + +class ContainsRouter(Router): + def match(self, input: str) -> bool: + return self.sign in input.strip() diff --git a/src/infini/settings.py b/src/infini/settings.py deleted file mode 100644 index 51c64dd2..00000000 --- a/src/infini/settings.py +++ /dev/null @@ -1 +0,0 @@ -DEBUG = True diff --git a/src/infini/typing.py b/src/infini/typing.py index 0da72385..0a0d3292 100644 --- a/src/infini/typing.py +++ b/src/infini/typing.py @@ -1,24 +1,22 @@ -"""Infini 类型提示支持。 - -此模块定义了部分 Infini 使用的类型。 -""" - from typing import ( Dict as Dict, + List as List, Any as Any, - Type as Type, - ClassVar as ClassVar, Generic as Generic, - TYPE_CHECKING as TYPE_CHECKING, - TypeVar as TypeVar, Callable as Callable, - NoReturn as NoReturn, - Awaitable as Awaitable, + Literal as Literal, + overload as overload, + TypeVar, + TypedDict, + Union, ) +from types import ModuleType as ModuleType +from . import router, input, output -if TYPE_CHECKING: - from typing import Any +T = TypeVar("T") -__all__ = ["StateT"] -StateT = TypeVar("StateT") +class RouterType(TypedDict): + priority: int + router: router.Router + handler: Callable[["input.Input"], Union["input.Input", "output.Output"]] diff --git a/src/infini/utils/cli.py b/src/infini/utils/cli.py deleted file mode 100644 index 3d30f839..00000000 --- a/src/infini/utils/cli.py +++ /dev/null @@ -1,28 +0,0 @@ -""" -Infini 终端命令解析模块 -""" - -import argparse -import sys - - -def parse_args(argv: list[str] | None = None) -> argparse.Namespace: - # sourcery skip: extract-duplicate-method - parser = argparse.ArgumentParser(prog="Infini CLI", description="Infini 命令行工具") - - parser.add_argument("--gui", action="store_true", help="启用 GUI 模式") - - subparsers = parser.add_subparsers(title="功能件", dest="operate") - - # 子命令 `new` - new_parser = subparsers.add_parser("new", help="创建一个 Infini 规则包模板") - new_parser.add_argument("path", help="目标位置") - new_parser.add_argument("-v", "--verbose", action="store_true", help="异常追踪") - new_parser.add_argument("-f", "--force", action="store_true", help="强制创建") - - # 子命令 `test` - test_parser = subparsers.add_parser("test", help="测试 Infini 规则包") - test_parser.add_argument("path", help="目标位置") - test_parser.add_argument("-v", "--verbose", action="store_true", help="异常追踪") - - return parser.parse_args(sys.argv[1:] or argv or ["--help"]) diff --git a/src/infini/utils/gui.py b/src/infini/utils/gui.py deleted file mode 100644 index 5e8c91c4..00000000 --- a/src/infini/utils/gui.py +++ /dev/null @@ -1,5 +0,0 @@ -""" -Infini 图形用户界面模块 -""" - -# TODO diff --git a/tests/test_handlers.py b/tests/test_handlers.py new file mode 100644 index 00000000..8d2d2894 --- /dev/null +++ b/tests/test_handlers.py @@ -0,0 +1,84 @@ +from infini.handler import Handler +from infini.input import Input +from infini.output import Output +from infini.router import StartswithRouter + + +def test_handler(): + input = Input(".add 1 2") + + def add(input: Input) -> Output: + a, b = map(int, input.get_plain_text().lstrip(".add").split()) + output = Output() + output.block = False + output.status = 0 + output.type = "text" + output.name = str(a + b) + return output + + def cmd(_: Input) -> Output: + output = Output() + output.block = False + output.status = 0 + output.type = "text" + output.name = "cmd" + return output + + handler = Handler() + handler.handlers = [ + { + "priority": 2, + "router": StartswithRouter(".add"), + "handler": add, + }, + { + "priority": 1, + "router": StartswithRouter("."), + "handler": cmd, + }, + ] + + names = [] + for output in handler.input(input): + names.append(output.name) + assert names == ["cmd", "3"] + + +def test_handler_block(): + input = Input(".add 1 2") + + def add(input: Input) -> Output: + a, b = map(int, input.get_plain_text().lstrip(".add").split()) + output = Output() + output.block = False + output.status = 0 + output.type = "text" + output.name = str(a + b) + return output + + def cmd(_: Input) -> Output: + output = Output() + output.block = True + output.status = 0 + output.type = "text" + output.name = "cmd" + return output + + handler = Handler() + handler.handlers = [ + { + "priority": 2, + "router": StartswithRouter(".add"), + "handler": add, + }, + { + "priority": 1, + "router": StartswithRouter("."), + "handler": cmd, + }, + ] + + names = [] + for output in handler.input(input): + names.append(output.name) + assert names == ["cmd"] diff --git a/tests/test_input.py b/tests/test_input.py new file mode 100644 index 00000000..5f44ca1e --- /dev/null +++ b/tests/test_input.py @@ -0,0 +1,16 @@ +from infini.input import Input + + +def test_new_input_without_vars(): + assert Input("test plain_str").plain_data == "test plain_str" + assert Input("test plain_str").get_plain_text() == "test plain_str" + + +def test_new_input_with_session_id(): + input = Input("test plain_str", variables={"session_id": "test"}) + assert input.get_session_id() == "test" + + +def test_new_input_without_session_id(): + input = Input("test plain_str", variables={"user_id": "test"}) + assert input.get_session_id() == "session_unknown_test" diff --git a/tests/test_interceptor.py b/tests/test_interceptor.py new file mode 100644 index 00000000..df5b6750 --- /dev/null +++ b/tests/test_interceptor.py @@ -0,0 +1,33 @@ +from infini.input import Input +from infini.interceptor import Interceptor +from infini.output import Output +from infini.router import ContainsRouter + + +def test_interceptor(): + input = Input("这个人叫简律纯.") + valid_input = Input("这个叫苏向夜.") + + def intercept(_: Input) -> Input | Output: + output = Output() + output.block = True # TODO 拦截器阻塞标识 + output.name = "block.jianlvchun" + output.status = 0 + output.type = "text" + return output + + interceptor = Interceptor() + interceptor.interceptors = [ + { + "priority": 1, + "router": ContainsRouter("简律纯"), + "handler": intercept, + } + ] + output = interceptor.input(input) + assert isinstance(output, Output) + assert output.name == "block.jianlvchun" + + valid_output = interceptor.input(valid_input) + assert isinstance(valid_output, Input) + assert valid_output.get_plain_text() == "这个叫苏向夜." |
