aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/hrc
diff options
context:
space:
mode:
Diffstat (limited to 'hrc')
-rw-r--r--hrc/LibCore.pyi5
-rw-r--r--hrc/__init__.py1
-rw-r--r--hrc/cli.py46
-rw-r--r--hrc/config.py34
-rw-r--r--hrc/const.py0
-rw-r--r--hrc/core.py737
-rw-r--r--hrc/dependencies.py115
-rw-r--r--hrc/dev/__init__.py1
-rw-r--r--hrc/dev/api/__init__.py0
-rw-r--r--hrc/dev/character.py2
-rw-r--r--hrc/dev/echo.py105
-rw-r--r--hrc/dev/grps/__init__.py1
-rw-r--r--hrc/dev/grps/v1.py12
-rw-r--r--hrc/doc/__init__.py1
-rw-r--r--hrc/event.py94
-rw-r--r--hrc/exceptions.py22
-rw-r--r--hrc/feat/__init__.py0
-rw-r--r--hrc/log.py22
-rw-r--r--hrc/perf/__init__.py0
-rw-r--r--hrc/py.typed0
-rw-r--r--hrc/rule/BaseRule/CharacterCard.py17
-rw-r--r--hrc/rule/BaseRule/CustomRule.py0
-rw-r--r--hrc/rule/BaseRule/Wiki.py0
-rw-r--r--hrc/rule/BaseRule/__init__.py3
-rw-r--r--hrc/rule/__init__.py164
-rw-r--r--hrc/service/__init__.py111
-rw-r--r--hrc/service/console/__init__.py41
-rw-r--r--hrc/service/http/__init__.py33
-rw-r--r--hrc/service/utils.py256
-rw-r--r--hrc/service/websocket/__init__.py30
-rw-r--r--hrc/typing.py23
-rw-r--r--hrc/utils.py291
32 files changed, 0 insertions, 2167 deletions
diff --git a/hrc/LibCore.pyi b/hrc/LibCore.pyi
deleted file mode 100644
index aa88747..0000000
--- a/hrc/LibCore.pyi
+++ /dev/null
@@ -1,5 +0,0 @@
-class LibCore(object):
- """Core library for hydro roll"""
-
- def __init__(self, name: str = ""): ...
- def process_rule_pack(self, rule_pack: str) -> str: ...
diff --git a/hrc/__init__.py b/hrc/__init__.py
deleted file mode 100644
index 89bc832..0000000
--- a/hrc/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from .LibCore import * # noqa: F403 \ No newline at end of file
diff --git a/hrc/cli.py b/hrc/cli.py
deleted file mode 100644
index 55758bc..0000000
--- a/hrc/cli.py
+++ /dev/null
@@ -1,46 +0,0 @@
-import argparse
-
-
-class Cli(object):
- parser = argparse.ArgumentParser(description="水系核心终端")
-
- def __init__(self):
- self.parser.add_argument(
- "-i",
- "--install",
- dest="command",
- help="安装规则包",
- action="store_const",
- const="install_package",
- )
- self.parser.add_argument(
- "-T",
- "--template",
- dest="command",
- help="选择模板快速创建规则包实例",
- action="store_const",
- const="build_template",
- )
- self.parser.add_argument(
- "-S",
- "--search",
- dest="command",
- help="在指定镜像源查找规则包",
- action="store_const",
- const="search_package",
- )
- self.parser.add_argument(
- "-c",
- "--config",
- dest="command",
- help="配置管理",
- action="store_const",
- const="config",
- )
- self.args = self.parser.parse_args()
-
- def get_args(self):
- return self.args
-
- def get_help(self):
- return self.parser.format_help()
diff --git a/hrc/config.py b/hrc/config.py
deleted file mode 100644
index d179258..0000000
--- a/hrc/config.py
+++ /dev/null
@@ -1,34 +0,0 @@
-from typing import Literal, Optional, Set, Union
-
-from pydantic import BaseModel, ConfigDict, DirectoryPath, Field
-
-
-class ConfigModel(BaseModel):
- model_config = ConfigDict(extra="allow")
-
- __config_name__: str = ""
-
-
-class LogConfig(ConfigModel):
- level: Union[str, int] = "DEBUG"
- verbose_exception: bool = False
-
-
-class ServiceConfig(ConfigModel):
- """Service configuration."""
-
-
-class CoreConfig(ConfigModel):
- rules: Set[str] = Field(default_factory=set)
- rule_dirs: Set[DirectoryPath] = Field(default_factory=set)
- log: LogConfig = LogConfig()
- services: Set[str] = Field(default_factory=set)
-
-class RuleConfig(ConfigModel):
- """Rule configuration."""
-
-
-class MainConfig(ConfigModel):
- core: CoreConfig = CoreConfig()
- rule: RuleConfig = RuleConfig()
- service: ServiceConfig = ServiceConfig()
diff --git a/hrc/const.py b/hrc/const.py
deleted file mode 100644
index e69de29..0000000
--- a/hrc/const.py
+++ /dev/null
diff --git a/hrc/core.py b/hrc/core.py
deleted file mode 100644
index 66ad211..0000000
--- a/hrc/core.py
+++ /dev/null
@@ -1,737 +0,0 @@
-import asyncio
-import json
-import pkgutil
-import signal
-import sys
-import threading
-import time # noqa: F401
-from collections import defaultdict
-from contextlib import AsyncExitStack
-from itertools import chain
-from pathlib import Path
-from typing import (
- Any,
- Awaitable, # noqa: F401
- Callable, # noqa: F401
- Dict,
- List,
- Optional,
- Set, # noqa: F401
- Tuple,
- Type,
- Union,
- overload, # noqa: F401
-)
-
-from pydantic import ValidationError, create_model
-
-from hrc.config import ConfigModel, MainConfig, RuleConfig, ServiceConfig
-from hrc.dependencies import solve_dependencies
-from hrc.log import logger, error_or_exception
-from hrc.rule import Rule, RuleLoadType
-from hrc.event import Event
-from hrc.typing import CoreHook, EventHook, EventT, RuleHook, ServiceT, ServiceHook # noqa: F401
-from hrc.utils import (
- ModulePathFinder,
- get_classes_from_module_name,
- is_config_class,
- samefile,
- wrap_get_func, # noqa: F401
-)
-from hrc.exceptions import StopException, SkipException, GetEventTimeout, LoadModuleError # noqa: F401
-from hrc.service import Service
-
-if sys.version_info >= (3, 11): # pragma: no cover
- import tomllib
-else: # pragma: no cover
- import tomli as tomllib
-
-HANDLED_SIGNALS = (
- signal.SIGINT, # Unix signal 2. Sent by Ctrl+C.
- signal.SIGTERM, # Unix signal 15. Sent by `kill <pid>`.
-)
-
-class Core:
- config: MainConfig
- _current_event: Optional[Event[Any]]
- _module_path_finder: ModulePathFinder
- _hot_reload: bool
- # pyright: ignore[reportUninitializedInstanceVariable]
- should_exit: asyncio.Event
- _restart_flag: bool # Restart flag
- _extend_services: List[
- Union[Type[Service[Any, Any]], str]
- ] # A list of services loaded programmatically using the ``load_service()`` method
- _extend_rules: List[Union[Type[Rule[Any, Any, Any]], str, Path]]
- _extend_rule_dirs: List[Path]
- rules_priority_dict: Dict[int, List[Type[Rule[Any, Any, Any]]]]
- _config_file: Optional[str] # Configuration file
- _config_dict: Optional[Dict[str, Any]] # Configuration dictionary
-
- _core_run_hooks: List[CoreHook]
- _core_exit_hooks: List[CoreHook]
- _service_startup_hooks: List[ServiceHook]
- _service_run_hooks: List[ServiceHook]
- _service_shutdown_hooks: List[ServiceHook]
- _event_preprocessor_hooks: List[EventHook]
- _event_postprocessor_hooks: List[EventHook]
-
- _service_tasks: Set[
- "asyncio.Task[None]"
- ] # Server task collection, used to hold references to server tasks
- _condition: (
- asyncio.Condition
- ) # Condition used to handle get # pyright: ignore[reportUninitializedInstanceVariable]
- _rule_tasks: Set[
- "asyncio.Task[None]"
- ] # Server task collection, used to hold references to server tasks
- _handle_event_tasks: Set[
- "asyncio.Task[None]"
- ] # Event handling task, used to keep a reference to the service task
-
- def __init__(
- self,
- *,
- config_file: Optional[str] = "config.toml",
- config_dict: Optional[Dict[str, Any]] = None,
- hot_reload: bool = False,
- ) -> None:
- self.config = MainConfig()
-
- self.services = []
- self._current_event = None
- self._config_file = config_file
- self._config_dict = config_dict
- self._hot_reload = hot_reload
- self._restart_flag = False
- self._module_path_finder = ModulePathFinder()
- self.rules_priority_dict = defaultdict(list)
- self._raw_config_dict = {}
- self._service_tasks = set()
- self._rule_tasks = set()
- self._handle_event_tasks = set()
-
- self._extend_services = []
- self._extend_rules = []
- self._extend_rule_dirs = []
- self._core_run_hooks = []
- self._core_exit_hooks = []
- self._service_startup_hooks = []
- self._service_run_hooks = []
- self._service_shutdown_hooks = []
- self._rule_enable_hooks = []
- self._rule_run_hooks = []
- self._rule_disable_hooks = []
- self._event_preprocessor_hooks = []
- self._event_postprocessor_hooks = []
-
- sys.meta_path.insert(0, self._module_path_finder)
-
- @property
- def rules(self) -> List[Type[Rule[Any, Any, Any]]]:
- return list(chain(*self.rules_priority_dict.values()))
-
- def run(self) -> None:
- self._restart_flag = True
- while self._restart_flag:
- self._restart_flag = False
- asyncio.run(self._run())
- if self._restart_flag:
- self._load_rules_from_dirs(*self._extend_rule_dirs)
- self._load_rules(*self._extend_rules)
- self._load_services(*self._extend_services)
-
- def restart(self) -> None:
- logger.info("Restarting...")
- self._restart_flag = True
- self.should_exit.set()
-
- async def _run(self) -> None:
- self.should_exit = asyncio.Event()
- self._condition = asyncio.Condition()
-
- # Monitor and intercept system exit signals to complete some aftermath work before closing the program
- if threading.current_thread() is threading.main_thread(): # pragma: no cover
- # Signals can only be processed in the main thread
- try:
- loop = asyncio.get_running_loop()
- for sig in HANDLED_SIGNALS:
- loop.add_signal_handler(sig, self._handle_exit)
- except NotImplementedError:
- # add_signal_handler is only available under Unix, below for Windows
- for sig in HANDLED_SIGNALS:
- signal.signal(sig, self._handle_exit)
-
- # Load configuration file
- self._reload_config_dict()
-
- self._load_rules_from_dirs(*self.config.core.rule_dirs)
- self._load_rules(*self.config.core.rules)
- self._load_services(*self.config.core.services)
- self._update_config()
-
- logger.info("Running...")
-
- hot_reload_task = None
- if self._hot_reload: # pragma: no cover
- hot_reload_task = asyncio.create_task(self._run_hot_reload())
-
- for core_run_hook_func in self._core_run_hooks:
- await core_run_hook_func(self)
-
- try:
- for _service in self.services:
- for _service_startup_hook_func in self._service_startup_hooks:
- await _service_startup_hook_func(_service)
- try:
- await _service.startup()
- except Exception as e:
- self.error_or_exception(f"Start service {_service!r} failed:", e)
-
- for _service in self.services:
- for _service_run_hook_func in self._service_run_hooks:
- await _service_run_hook_func(_service)
- _service_task = asyncio.create_task(_service.safe_run())
- self._service_tasks.add(_service_task)
- _service_task.add_done_callback(self._service_tasks.discard)
-
- # TODO(简律纯): builtin rule enable hook function in every rules packages.
- # for _rule in self.rules:
- # for rule_enable_hook_func in self._rule_enable_hooks:
- # await rule_enable_hook_func(_rule)
- # try:
- # await _rule.enable()
- # except Exception as e:
- # self.error_or_exception(
- # f"Enable rule {_rule!r} failed:", e)
- # TODO(简律纯): builtin rule run hook function in every rules packages.
- # for _rule in self.rules:
- # for rule_run_hook_func in self._rule_run_hooks:
- # await rule_run_hook_func(_rule)
- # _rule_task = asyncio.create_task(_rule.safe_run())
- # self._rule_tasks.add(_rule_task)
- # _rule_task.add_done_callback(self._rule_tasks.discard)
-
- await self.should_exit.wait()
-
- if hot_reload_task is not None: # pragma: no cover
- await hot_reload_task
- finally:
- for _service in self.services:
- for service_shutdown_hook_func in self._service_shutdown_hooks:
- await service_shutdown_hook_func(_service)
- await _service.shutdown()
-
- # TODO(简律纯): builtin rule disable hook function in every rules packages.
- # for _rule in self.rules:
- # for rule_disable_hook_func in self._rule_disable_hooks:
- # await rule_disable_hook_func(_rule)
- # await _rule.disable()
-
- while self._service_tasks:
- await asyncio.sleep(0)
-
- while self._rule_tasks:
- await asyncio.sleep(0)
-
- for core_exit_hook_func in self._core_exit_hooks:
- await core_exit_hook_func(self)
-
- self.services.clear()
- self.rules.clear()
- self.rules_priority_dict.clear()
- self._module_path_finder.path.clear()
-
- def _remove_rule_by_path(
- self, file: Path
- ) -> List[Type[Rule[Any, Any, Any]]]: # pragma: no cover
- removed_rules: List[Type[Rule[Any, Any, Any]]] = []
- for rules in self.rules_priority_dict.values():
- _removed_rules = list(
- filter(
- lambda x: x.__rule_load_type__ != RuleLoadType.CLASS
- and x.__rule_file_path__ is not None
- and samefile(x.__rule_file_path__, file),
- rules,
- )
- )
- removed_rules.extend(_removed_rules)
- for rule_ in _removed_rules:
- rules.remove(rule_)
- logger.info(
- "Succeeded to remove rule " f'"{rule_.__name__}" from file "{file}"'
- )
- return removed_rules
-
- async def _run_hot_reload(self) -> None: # pragma: no cover
- """Hot reload."""
- try:
- from watchfiles import Change, awatch
- except ImportError:
- logger.warning(
- 'Hot reload needs to install "watchfiles", try "pip install watchfiles"'
- )
- return
-
- logger.info("Hot reload is working!")
- async for changes in awatch(
- *(
- x.resolve()
- for x in set(self._extend_rule_dirs)
- .union(self.config.core.rule_dirs)
- .union(
- {Path(self._config_file)}
- if self._config_dict is None and self._config_file is not None
- else set()
- )
- ),
- stop_event=self.should_exit,
- ):
- # Processed in the order of Change.deleted, Change.modified, Change.added
- # To ensure that when renaming occurs, deletions are processed first and then additions are processed
- for change_type, file_ in sorted(changes, key=lambda x: x[0], reverse=True):
- file = Path(file_)
- # Change configuration file
- if (
- self._config_file is not None
- and samefile(self._config_file, file)
- and change_type == change_type.modified
- ):
- logger.info(f'Reload config file "{self._config_file}"')
- old_config = self.config
- self._reload_config_dict()
- if (
- self.config.core != old_config.core
- or self.config.rule != old_config.rule
- ):
- self.restart()
- continue
-
- # Change rule folder
- if change_type == Change.deleted:
- # Special handling for deletion operations
- if file.suffix != ".py":
- file = file / "__init__.py"
- else:
- if file.is_dir() and (file / "__init__.py").is_file():
- # When a new directory is added and this directory contains the ``__init__.py`` file
- # It means that what happens at this time is that a Python package is added, and the ``__init__.py`` file of this package is deemed to be added
- file = file / "__init__.py"
- if not (file.is_file() and file.suffix == ".py"):
- continue
-
- if change_type == Change.added:
- logger.info(f"Hot reload: Added file: {file}")
- self._load_rules(
- Path(file), rule_load_type=RuleLoadType.DIR, reload=True
- )
- self._update_config()
- continue
- if change_type == Change.deleted:
- logger.info(f"Hot reload: Deleted file: {file}")
- self._remove_rule_by_path(file)
- self._update_config()
- elif change_type == Change.modified:
- logger.info(f"Hot reload: Modified file: {file}")
- self._remove_rule_by_path(file)
- self._load_rules(
- Path(file), rule_load_type=RuleLoadType.DIR, reload=True
- )
- self._update_config()
-
- def _update_config(self) -> None:
- def update_config(
- source: Union[List[Type[Rule[Any, Any, Any]]], List[Service[Any, Any]]],
- name: str,
- base: Type[ConfigModel],
- ) -> Tuple[Type[ConfigModel], ConfigModel]:
- config_update_dict: Dict[str, Any] = {}
- for i in source:
- config_class = getattr(i, "Config", None)
- if is_config_class(config_class):
- default_value: Any
- try:
- default_value = config_class()
- except ValidationError:
- default_value = ...
- config_update_dict[config_class.__config_name__] = (
- config_class,
- default_value,
- )
- config_model = create_model(
- name, **config_update_dict, __base__=base)
- return config_model, config_model()
-
- self.config = create_model(
- "Config",
- rule=update_config(self.rules, "RuleConfig", RuleConfig),
- service=update_config(self.services, "ServiceConfig", ServiceConfig),
- __base__=MainConfig,
- )(**self._raw_config_dict)
- # Update the level of logging
- logger.remove()
- logger.add(sys.stderr, level=self.config.core.log.level)
-
- def _reload_config_dict(self) -> None:
- """Reload the configuration file."""
- self._raw_config_dict = {}
-
- if self._config_dict is not None:
- self._raw_config_dict = self._config_dict
- elif self._config_file is not None:
- try:
- with Path(self._config_file).open("rb") as f:
- if self._config_file.endswith(".json"):
- self._raw_config_dict = json.load(f)
- elif self._config_file.endswith(".toml"):
- self._raw_config_dict = tomllib.load(f)
- else:
- self.error_or_exception(
- "Read config file failed:",
- OSError("Unable to determine config file type"),
- )
- except OSError as e:
- self.error_or_exception("Can not open config file:", e)
- except (ValueError, json.JSONDecodeError, tomllib.TOMLDecodeError) as e:
- self.error_or_exception("Read config file failed:", e)
-
- try:
- self.config = MainConfig(**self._raw_config_dict)
- except ValidationError as e:
- self.config = MainConfig()
- self.error_or_exception("Config dict parse error:", e)
- self._update_config()
-
- def reload_rules(self) -> None:
- """Manually reload all rules."""
- self.rules_priority_dict.clear()
- self._load_rules(*self.config.core.rules)
- self._load_rules_from_dirs(*self.config.core.rule_dirs)
- self._load_rules(*self._extend_rules)
- self._load_rules_from_dirs(*self._extend_rule_dirs)
- self._update_config()
-
- def _handle_exit(self, *_args: Any) -> None: # pragma: no cover
- """When the robot receives the exit signal, it will handle it according to the situation."""
- logger.info("Stopping...")
- if self.should_exit.is_set():
- logger.warning("Force Exit...")
- sys.exit()
- else:
- self.should_exit.set()
-
- async def handle_event(
- self,
- current_event: Event[Any],
- *,
- handle_get: bool = True,
- show_log: bool = True,
- ) -> None:
- if show_log:
- logger.info(
- f"Service {current_event.service.name} received: {current_event!r}")
-
- if handle_get:
- _handle_event_task = asyncio.create_task(self._handle_event())
- self._handle_event_tasks.add(_handle_event_task)
- _handle_event_task.add_done_callback(
- self._handle_event_tasks.discard)
- await asyncio.sleep(0)
- async with self._condition:
- self._current_event = current_event
- self._condition.notify_all()
- else:
- _handle_event_task = asyncio.create_task(
- self._handle_event(current_event))
- self._handle_event_tasks.add(_handle_event_task)
- _handle_event_task.add_done_callback(
- self._handle_event_tasks.discard)
-
- async def _handle_event(self, current_event: Optional[Event[Any]] = None) -> None:
- if current_event is None:
- async with self._condition:
- await self._condition.wait()
- assert self._current_event is not None
- current_event = self._current_event
- if current_event.__handled__:
- return
-
- for _hook_func in self._event_preprocessor_hooks:
- await _hook_func(current_event)
-
- for rule_priority in sorted(self.rules_priority_dict.keys()):
- logger.debug(
- f"Checking for matching rules with priority {rule_priority!r}"
- )
- stop = False
- for rule in self.rules_priority_dict[rule_priority]:
- try:
- async with AsyncExitStack() as stack:
- _rule = await solve_dependencies(
- rule,
- use_cache=True,
- stack=stack,
- dependency_cache={
- Core: self,
- Event: current_event,
- },
- )
- if _rule.name not in self.rule_state:
- rule_state = _rule.__init_state__()
- if rule_state is not None:
- self.rule_state[_rule.name] = rule_state
- # TODO(简律纯): Refactor event handle process with General Rules Package Standard
- if await _rule.rule():
- logger.info(f"Event will be handled by {_rule!r}")
- try:
- await _rule.handle()
- finally:
- if _rule.block:
- stop = True
- except SkipException:
- # The plug-in requires that it skips itself and continues the current event propagation
- continue
- except StopException:
- # rule requires stopping current event propagation
- stop = True
- except Exception as e:
- self.error_or_exception(f'Exception in rule "{rule}":', e)
- if stop:
- break
-
- for _hook_func in self._event_postprocessor_hooks:
- await _hook_func(current_event)
-
- logger.info("Event Finished")
-
- def _load_rule_class(
- self,
- rule_class: Type[Rule[Any, Any, Any]],
- rule_load_type: RuleLoadType,
- rule_file_path: Optional[str],
- ) -> None:
- """Load a rule class"""
- priority = getattr(rule_class, "priority", None)
- if isinstance(priority, int) and priority >= 0:
- for _rule in self.rules:
- if _rule.__name__ == rule_class.__name__:
- logger.warning(
- f'Already have a same name rule "{_rule.__name__}"'
- )
- rule_class.__rule_load_type__ = rule_load_type
- rule_class.__rule_file_path__ = rule_file_path
- self.rules_priority_dict[priority].append(rule_class)
- logger.info(
- f'Succeeded to load rule "{rule_class.__name__}" '
- f'from class "{rule_class!r}"'
- )
- else:
- self.error_or_exception(
- f'Load rule from class "{rule_class!r}" failed:',
- LoadModuleError(
- f'rule priority incorrect in the class "{rule_class!r}"'
- ),
- )
-
- def _load_rules_from_module_name(
- self,
- module_name: str,
- *,
- rule_load_type: RuleLoadType,
- reload: bool = False,
- ) -> None:
- """Load rules from the given module."""
- try:
- rule_classes = get_classes_from_module_name(
- module_name, Rule, reload=reload
- )
- except ImportError as e:
- self.error_or_exception(
- f'Import module "{module_name}" failed:', e)
- else:
- for rule_class, module in rule_classes:
- self._load_rule_class(
- rule_class, # type: ignore
- rule_load_type,
- module.__file__,
- )
-
- def _load_rules(
- self,
- *rules: Union[Type[Rule[Any, Any, Any]], str, Path],
- rule_load_type: Optional[RuleLoadType] = None,
- reload: bool = False,
- ) -> None:
- for rule_ in rules:
- try:
- if isinstance(rule_, type) and issubclass(rule_, Rule):
- self._load_rule_class(
- rule_, rule_load_type or RuleLoadType.CLASS, None
- )
- elif isinstance(rule_, str):
- logger.info(f'Loading rules from module "{rule_}"')
- self._load_rules_from_module_name(
- rule_,
- rule_load_type=rule_load_type or RuleLoadType.NAME,
- reload=reload,
- )
- elif isinstance(rule_, Path):
- logger.info(f'Loading rules from path "{rule_}"')
- if not rule_.is_file():
- raise LoadModuleError( # noqa: TRY301
- f'The rule path "{rule_}" must be a file'
- )
-
- if rule_.suffix != ".py":
- raise LoadModuleError( # noqa: TRY301
- f'The path "{rule_}" must endswith ".py"'
- )
-
- rule_module_name = None
- for path in self._module_path_finder.path:
- try:
- if rule_.stem == "__init__":
- if rule_.resolve().parent.parent.samefile(Path(path)):
- rule_module_name = rule_.resolve().parent.name
- break
- elif rule_.resolve().parent.samefile(Path(path)):
- rule_module_name = rule_.stem
- break
- except OSError:
- continue
- if rule_module_name is None:
- rel_path = rule_.resolve().relative_to(Path().resolve())
- if rel_path.stem == "__init__":
- rule_module_name = ".".join(rel_path.parts[:-1])
- else:
- rule_module_name = ".".join(
- rel_path.parts[:-1] + (rel_path.stem,)
- )
-
- self._load_rules_from_module_name(
- rule_module_name,
- rule_load_type=rule_load_type or RuleLoadType.FILE,
- reload=reload,
- )
- else:
- raise TypeError( # noqa: TRY301
- f"{rule_} can not be loaded as rule"
- )
- except Exception as e:
- self.error_or_exception(f'Load rule "{rule_}" failed:', e)
-
- def load_rules(
- self, *rules: Union[Type[Rule[Any, Any, Any]], str, Path]
- ) -> None:
- self._extend_rules.extend(rules)
-
- return self._load_rules(*rules)
-
- def _load_rules_from_dirs(self, *dirs: Path) -> None:
- dir_list = [str(x.resolve()) for x in dirs]
- logger.info(
- f'Loading rules from dirs "{", ".join(map(str, dir_list))}"')
- self._module_path_finder.path.extend(dir_list)
- for module_info in pkgutil.iter_modules(dir_list):
- if not module_info.name.startswith("_"):
- self._load_rules_from_module_name(
- module_info.name, rule_load_type=RuleLoadType.DIR
- )
-
- def load_rules_from_dirs(self, *dirs: Path) -> None:
- self._extend_rule_dirs.extend(dirs)
- self._load_rules_from_dirs(*dirs)
-
- def _load_services(self, *services: Union[Type[Service[Any, Any]], str]) -> None:
- for service_ in services:
- service_object: Service[Any, Any]
- try:
- if isinstance(service_, type) and issubclass(service_, Service):
- service_object = service_(self)
- elif isinstance(service_, str):
- service_classes = get_classes_from_module_name(service_, Service)
- if not service_classes:
- raise LoadModuleError( # noqa: TRY301
- f"Can not find Service class in the {service_} module"
- )
- if len(service_classes) > 1:
- raise LoadModuleError( # noqa: TRY301
- f"More then one Service class in the {service_} module"
- )
- service_object = service_classes[0][0](self) # type: ignore
- else:
- raise TypeError( # noqa: TRY301
- f"{service_} can not be loaded as service"
- )
- except Exception as e:
- self.error_or_exception(f'Load service "{service_}" failed:', e)
- else:
- self.services.append(service_object)
- logger.info(
- f'Succeeded to load service "{service_object.__class__.__name__}" '
- f'from "{service_}"'
- )
-
- def load_services(self, *services: Union[Type[Service[Any, Any]], str]) -> None:
- self._extend_services.extend(services)
- self._load_services(*services)
-
- @overload
- def get_service(self, service: str) -> Service[Any, Any]: ...
-
- @overload
- def get_service(self, service: Type[ServiceT]) -> ServiceT: ...
-
- def get_service(
- self, service: Union[str, Type[ServiceT]]
- ) -> Union[Service[Any, Any], ServiceT]:
- for _service in self.services:
- if isinstance(service, str):
- if _service.name == service:
- return _service
- elif isinstance(_service, service):
- return _service
- raise LookupError(f'Can not find service named "{service}"')
-
-
- def get_rule(self, name: str) -> Type[Rule[Any, Any, Any]]:
- for _rule in self.rules:
- if _rule.__name__ == name:
- return _rule
- raise LookupError(f'Can not find rule named "{name}"')
-
- def error_or_exception(
- self, message: str, exception: Exception
- ) -> None: # pragma: no cover
- if self.config.core.log.verbose_exception:
- error_or_exception(message)
- else:
- error_or_exception(message, exception, verbose=False)
-
- def core_run_hook(self, func: CoreHook) -> CoreHook:
- self._core_run_hooks.append(func)
- return func
-
- def core_exit_hook(self, func: CoreHook) -> CoreHook:
- self._core_exit_hooks.append(func)
- return func
-
- def rule_enable_hook(self, func: RuleHook) -> RuleHook:
- self._rule_enable_hooks.append(func)
- return func
-
- def rule_run_hook(self, func: RuleHook) -> RuleHook:
- self._rule_run_hooks.append(func)
- return func
-
- def rule_disable_hook(self, func: RuleHook) -> RuleHook:
- self._rule_disable_hooks.append(func)
- return func
-
- def event_preprocessor_hook(self, func: EventHook) -> EventHook:
- self._event_preprocessor_hooks.append(func)
- return func
-
- def event_postprocessor_hook(self, func: EventHook) -> EventHook:
- self._event_postprocessor_hooks.append(func)
- return func
diff --git a/hrc/dependencies.py b/hrc/dependencies.py
deleted file mode 100644
index e176e14..0000000
--- a/hrc/dependencies.py
+++ /dev/null
@@ -1,115 +0,0 @@
-import inspect
-from contextlib import AsyncExitStack, asynccontextmanager, contextmanager
-from typing import (
- Any,
- AsyncContextManager,
- AsyncGenerator,
- Callable,
- ContextManager,
- Dict,
- Generator,
- Optional,
- Type,
- TypeVar,
- Union,
- cast,
-)
-
-from hrc.utils import get_annotations, sync_ctx_manager_wrapper
-
-_T = TypeVar("_T")
-Dependency = Union[
- # Class
- Type[Union[_T, AsyncContextManager[_T], ContextManager[_T]]],
- # GeneratorContextManager
- Callable[[], AsyncGenerator[_T, None]],
- Callable[[], Generator[_T, None, None]],
-]
-
-
-__all__ = ["Depends"]
-
-
-class InnerDepends:
-
- dependency: Optional[Dependency[Any]]
- use_cache: bool
-
- def __init__(
- self, dependency: Optional[Dependency[Any]] = None, *, use_cache: bool = True
- ) -> None:
- self.dependency = dependency
- self.use_cache = use_cache
-
- def __repr__(self) -> str:
- attr = getattr(self.dependency, "__name__", type(self.dependency).__name__)
- cache = "" if self.use_cache else ", use_cache=False"
- return f"InnerDepends({attr}{cache})"
-
-
-def Depends(dependency: Optional[Dependency[_T]] = None, *, use_cache: bool = True) -> _T:
- return InnerDepends(dependency=dependency, use_cache=use_cache) # type: ignore
-
-
-async def solve_dependencies(
- dependent: Dependency[_T],
- *,
- use_cache: bool,
- stack: AsyncExitStack,
- dependency_cache: Dict[Dependency[Any], Any],
-) -> _T:
- if use_cache and dependent in dependency_cache:
- return dependency_cache[dependent]
-
- if isinstance(dependent, type):
- # type of dependent is Type[T]
- values: Dict[str, Any] = {}
- ann = get_annotations(dependent)
- for name, sub_dependent in inspect.getmembers(
- dependent, lambda x: isinstance(x, InnerDepends)
- ):
- assert isinstance(sub_dependent, InnerDepends)
- if sub_dependent.dependency is None:
- dependent_ann = ann.get(name, None)
- if dependent_ann is None:
- raise TypeError("can not solve dependent")
- sub_dependent.dependency = dependent_ann
- values[name] = await solve_dependencies(
- sub_dependent.dependency,
- use_cache=sub_dependent.use_cache,
- stack=stack,
- dependency_cache=dependency_cache,
- )
- depend_obj = cast(
- Union[_T, AsyncContextManager[_T], ContextManager[_T]],
- dependent.__new__(dependent), # pyright: ignore[reportGeneralTypeIssues]
- )
- for key, value in values.items():
- setattr(depend_obj, key, value)
- depend_obj.__init__() # type: ignore[misc] # pylint: disable=unnecessary-dunder-call
-
- if isinstance(depend_obj, AsyncContextManager):
- depend = await stack.enter_async_context(
- depend_obj # pyright: ignore[reportUnknownArgumentType]
- )
- elif isinstance(depend_obj, ContextManager):
- depend = await stack.enter_async_context(
- sync_ctx_manager_wrapper(
- depend_obj # pyright: ignore[reportUnknownArgumentType]
- )
- )
- else:
- depend = depend_obj
- elif inspect.isasyncgenfunction(dependent):
- # type of dependent is Callable[[], AsyncGenerator[T, None]]
- cm = asynccontextmanager(dependent)()
- depend = cast(_T, await stack.enter_async_context(cm))
- elif inspect.isgeneratorfunction(dependent):
- # type of dependent is Callable[[], Generator[T, None, None]]
- cm = sync_ctx_manager_wrapper(contextmanager(dependent)())
- depend = cast(_T, await stack.enter_async_context(cm))
- else:
- raise TypeError("dependent is not a class or generator function")
-
- dependency_cache[dependent] = depend
- return depend \ No newline at end of file
diff --git a/hrc/dev/__init__.py b/hrc/dev/__init__.py
deleted file mode 100644
index ea8f56b..0000000
--- a/hrc/dev/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from hrc.dev.grps import v1 \ No newline at end of file
diff --git a/hrc/dev/api/__init__.py b/hrc/dev/api/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/hrc/dev/api/__init__.py
+++ /dev/null
diff --git a/hrc/dev/character.py b/hrc/dev/character.py
deleted file mode 100644
index c883e45..0000000
--- a/hrc/dev/character.py
+++ /dev/null
@@ -1,2 +0,0 @@
-class Character:
- class Attribute: ...
diff --git a/hrc/dev/echo.py b/hrc/dev/echo.py
deleted file mode 100644
index 5bdeab7..0000000
--- a/hrc/dev/echo.py
+++ /dev/null
@@ -1,105 +0,0 @@
-"""HydroRoll-Team/echo
-水系跨平台事件标准(cross-platform event standard): Event Communication and Harmonization across Online platforms.
-:ref: https://github/com/HydroRoll-Team/echo
-:ref: https://echo.hydroroll.team
-"""
-
-class Event(object):
- """事件基类
- :ref: https://echo.hydroroll.team/Event/#0_event
- """
- def __init__(self, event_type, data, metadata):
- self.event_type = event_type
- self.data = data
- self.metadata = metadata
-
-class WorkFlow(Event):
- """workflow
- :ref: https://echo.hydroroll.team/Event/#1_workflow
- """
- def __init__(self, data, metadata):
- super().__init__('workflow', data, metadata)
-
-class CallBack(Event):
- """callback
- :ref: https://echo.hydroroll.team/Event/#4_callback
- """
- def __init__(self, data, metadata):
- super().__init__('callback', data, metadata)
-
-class Message(Event):
- """message
- :ref: https://echo.hydroroll.team/Event/#2_message
- """
- def __init__(self, data, metadata):
- super().__init__('message', data, metadata)
-
-class Reaction(Event):
- """reaction
- :ref: https://echo.hydroroll.team/Event/#3_reaction
- """
- def __init__(self, data, metadata):
- super().__init__('reaction', data, metadata)
-
-class Typing(Event):
- """typing
- :ref: https://echo.hydroroll.team/Event/#5_typing
- """
- def __init__(self, data, metadata):
- super().__init__('typing', data, metadata)
-
-class UserJoin(Event):
- """user join
- :ref: https://echo.hydroroll.team/Event/#6_user_join
- """
- def __init__(self, data, metadata):
- super().__init__('user_join', data, metadata)
-
-class UserLeave(Event):
- """user leave
- :ref: https://echo.hydroroll.team/Event/#7_user_leave
- """
- def __init__(self, data, metadata):
- super().__init__('user_leave', data, metadata)
-
-class FileShare(Event):
- """file share
- :ref: https://echo.hydroroll.team/Event/#8_file_share
- """
- def __init__(self, data, metadata):
- super().__init__('file_share', data, metadata)
-
-class Mention(Event):
- """mention
- :ref: https://echo.hydroroll.team/Event/#9_mention
- """
- def __init__(self, data, metadata):
- super().__init__('mention', data, metadata)
-
-class ChannelCreate(Event):
- """channel create
- :ref: https://echo.hydroroll.team/Event/#10_channel_create
- """
- def __init__(self, data, metadata):
- super().__init__('channel_create', data, metadata)
-
-class ChannelDelete(Event):
- """channel delete
- :ref: https://echo.hydroroll.team/Event/#11_channel_delete
- """
- def __init__(self, data, metadata):
- super().__init__('channel_delete', data, metadata)
-
-class ChannelUpdate(Event):
- """channel update
- :ref: https://echo.hydroroll.team/Event/#12_channel_update
- """
- def __init__(self, data, metadata):
- super().__init__('channel_update', data, metadata)
-
-class UserUpdate(Event):
- """user update
- :ref: https://echo.hydroroll.team/Event/#13_user_update
- """
- def __init__(self, data, metadata):
- super().__init__('user_update', data, metadata) \ No newline at end of file
diff --git a/hrc/dev/grps/__init__.py b/hrc/dev/grps/__init__.py
deleted file mode 100644
index bbf8c7e..0000000
--- a/hrc/dev/grps/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from . import v1 \ No newline at end of file
diff --git a/hrc/dev/grps/v1.py b/hrc/dev/grps/v1.py
deleted file mode 100644
index fa987a7..0000000
--- a/hrc/dev/grps/v1.py
+++ /dev/null
@@ -1,12 +0,0 @@
-from datetime import datetime
-from pydantic import BaseModel
-
-
-__version__ = "1.0.0-alpha.1"
-
-class GRPS(BaseModel):
- id: str
- name: str
- description: str
- created_at: datetime
- updated_at: datetime \ No newline at end of file
diff --git a/hrc/doc/__init__.py b/hrc/doc/__init__.py
deleted file mode 100644
index 998b999..0000000
--- a/hrc/doc/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-import sphinx \ No newline at end of file
diff --git a/hrc/event.py b/hrc/event.py
deleted file mode 100644
index 961a356..0000000
--- a/hrc/event.py
+++ /dev/null
@@ -1,94 +0,0 @@
-from abc import ABC, abstractmethod
-from typing import TYPE_CHECKING, Any, Generic, Optional, Union
-from typing_extensions import Self
-
-from pydantic import BaseModel, ConfigDict
-from hrc.typing import RuleT
-
-
-class Event(ABC, BaseModel, Generic[RuleT]):
- model_config = ConfigDict(extra="allow")
-
- if TYPE_CHECKING:
- rule: RuleT
- else:
- rule: Any
- type: Optional[str]
- __handled__: bool = False
-
- def __str__(self) -> str:
- return f"Event<{self.type}>"
-
- def __repr__(self) -> str:
- return self.__str__()
-
-
-class MessageEvent(Event[RuleT], Generic[RuleT]):
- """Base class for general message event classes."""
-
- @abstractmethod
- def get_plain_text(self) -> str:
- """Get the plain text content of the message.
-
- Returns:
- The plain text content of the message.
- """
-
- @abstractmethod
- async def reply(self, message: str) -> Any:
- """Reply message.
-
- Args:
- message: The content of the reply message.
-
- Returns:
- The response to the reply message action.
- """
-
- @abstractmethod
- async def is_same_sender(self, other: Self) -> bool:
- """Determine whether itself and another event are the same sender.
-
- Args:
- other: another event.
-
- Returns:
- Is it the same sender?
- """
-
- async def get(
- self,
- *,
- max_try_times: Optional[int] = None,
- timeout: Optional[Union[int, float]] = None,
- ) -> Self:
-
- return await self.rule.get(
- self.is_same_sender,
- event_type=type(self),
- max_try_times=max_try_times,
- timeout=timeout,
- )
-
- async def ask(
- self,
- message: str,
- max_try_times: Optional[int] = None,
- timeout: Optional[Union[int, float]] = None,
- ) -> Self:
- """Ask for news.
-
- Indicates getting the user's reply after replying to a message.
- Equivalent to executing ``get()`` after ``reply()``.
-
- Args:
- message: The content of the reply message.
- max_try_times: Maximum number of events.
- timeout: timeout period.
-
- Returns:
- Message event that the user replies to.
- """
-
- await self.reply(message)
- return await self.get(max_try_times=max_try_times, timeout=timeout)
diff --git a/hrc/exceptions.py b/hrc/exceptions.py
deleted file mode 100644
index c71118f..0000000
--- a/hrc/exceptions.py
+++ /dev/null
@@ -1,22 +0,0 @@
-class EventException(BaseException):
- ...
-
-
-class SkipException(EventException):
- ...
-
-
-class StopException(EventException):
- ...
-
-
-class CoreException(Exception):
- ... # noqa: N818
-
-
-class GetEventTimeout(CoreException):
- ...
-
-
-class LoadModuleError(CoreException):
- ...
diff --git a/hrc/feat/__init__.py b/hrc/feat/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/hrc/feat/__init__.py
+++ /dev/null
diff --git a/hrc/log.py b/hrc/log.py
deleted file mode 100644
index 8e476a6..0000000
--- a/hrc/log.py
+++ /dev/null
@@ -1,22 +0,0 @@
-import os
-import sys
-from datetime import datetime
-from typing import Optional
-
-from loguru import logger as _logger
-
-logger = _logger
-
-current_path = os.path.dirname(os.path.abspath("__file__"))
-log_path = os.path.join(
- current_path, "logs", datetime.now().strftime("%Y-%m-%d") + ".log"
-)
-
-def error_or_exception(message: str, exception: Optional[Exception], verbose: bool = True):
- logger.remove()
- logger.add(sys.stderr)
- logger.add(sink=log_path, level="INFO", rotation="10 MB")
- if verbose:
- logger.exception(message)
- else:
- logger.critical(f"{message} {exception!r}")
diff --git a/hrc/perf/__init__.py b/hrc/perf/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/hrc/perf/__init__.py
+++ /dev/null
diff --git a/hrc/py.typed b/hrc/py.typed
deleted file mode 100644
index e69de29..0000000
--- a/hrc/py.typed
+++ /dev/null
diff --git a/hrc/rule/BaseRule/CharacterCard.py b/hrc/rule/BaseRule/CharacterCard.py
deleted file mode 100644
index 2baea48..0000000
--- a/hrc/rule/BaseRule/CharacterCard.py
+++ /dev/null
@@ -1,17 +0,0 @@
-from dataclasses import dataclass
-
-
-@dataclass
-class Custom(object):
- """Docstring for Custom."""
-
- property: type
-
-
-class Attribute(Custom): ...
-
-
-class Skill(Custom): ...
-
-
-class Information(Custom): ...
diff --git a/hrc/rule/BaseRule/CustomRule.py b/hrc/rule/BaseRule/CustomRule.py
deleted file mode 100644
index e69de29..0000000
--- a/hrc/rule/BaseRule/CustomRule.py
+++ /dev/null
diff --git a/hrc/rule/BaseRule/Wiki.py b/hrc/rule/BaseRule/Wiki.py
deleted file mode 100644
index e69de29..0000000
--- a/hrc/rule/BaseRule/Wiki.py
+++ /dev/null
diff --git a/hrc/rule/BaseRule/__init__.py b/hrc/rule/BaseRule/__init__.py
deleted file mode 100644
index fdde86c..0000000
--- a/hrc/rule/BaseRule/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from . import CharacterCard # noqa: F401
-from . import CustomRule # noqa: F401
-from . import Wiki # noqa: F401
diff --git a/hrc/rule/__init__.py b/hrc/rule/__init__.py
deleted file mode 100644
index 282e3e2..0000000
--- a/hrc/rule/__init__.py
+++ /dev/null
@@ -1,164 +0,0 @@
-import functools # noqa: F401
-from typing import Generic, Any, Type
-
-from abc import ABC
-
-from hrc.rule import BaseRule # noqa: F401
-from hrc.typing import RuleT # noqa: F401
-
-import inspect
-from abc import abstractmethod # noqa: F401
-from enum import Enum
-from typing import (
- TYPE_CHECKING,
- ClassVar,
- NoReturn,
- Optional,
- Tuple,
- cast,
- final,
-)
-from typing_extensions import Annotated, get_args, get_origin
-
-from hrc.config import ConfigModel
-
-from hrc.dependencies import Depends
-from hrc.event import Event
-from hrc.exceptions import SkipException, StopException
-from hrc.typing import ConfigT, EventT, StateT
-from hrc.utils import is_config_class
-
-if TYPE_CHECKING:
- from hrc.core import Core
-
-
-class RuleLoadType(Enum):
- """Rules loaded types."""
-
- DIR = "dir"
- NAME = "name"
- FILE = "file"
- CLASS = "class"
-
-
-class Rule(ABC, Generic[EventT, StateT, ConfigT]):
- priority: ClassVar[int] = 0
- block: ClassVar[bool] = False
-
- # Cannot use ClassVar because PEP 526 does not allow it
- Config: Type[ConfigT]
-
- __rule_load_type__: ClassVar[RuleLoadType]
- __rule_file_path__: ClassVar[Optional[str]]
-
- if TYPE_CHECKING:
- event: EventT
- else:
- event = Depends(Event) # noqa: F821
-
- def __init_state__(self) -> Optional[StateT]:
- """Initialize rule state."""
-
- def __init_subclass__(
- cls,
- config: Optional[Type[ConfigT]] = None,
- init_state: Optional[StateT] = None,
- **_kwargs: Any,
- ) -> None:
- super().__init_subclass__()
-
- orig_bases: Tuple[type, ...] = getattr(cls, "__orig_bases__", ())
- for orig_base in orig_bases:
- origin_class = get_origin(orig_base)
- if inspect.isclass(origin_class) and issubclass(origin_class, Rule):
- try:
- _event_t, state_t, config_t = cast(
- Tuple[EventT, StateT, ConfigT], get_args(orig_base)
- )
- except ValueError: # pragma: no cover
- continue
- if (
- config is None
- and inspect.isclass(config_t)
- and issubclass(config_t, ConfigModel)
- ):
- config = config_t # pyright: ignore
- if (
- init_state is None
- and get_origin(state_t) is Annotated
- and hasattr(state_t, "__metadata__")
- ):
- init_state = state_t.__metadata__[0] # pyright: ignore
-
- if not hasattr(cls, "Config") and config is not None:
- cls.Config = config
- if cls.__init_state__ is Rule.__init_state__ and init_state is not None:
- cls.__init_state__ = lambda _: init_state # type: ignore
-
- @final
- @property
- def name(self) -> str:
- """rule class name."""
- return self.__class__.__name__
-
- @final
- @property
- def core(self) -> "Core":
- """core object."""
- return self.event.core # pylint: disable=no-member
-
- @final
- @property
- def config(self) -> ConfigT:
- """rule configuration."""
- default: Any = None
- config_class = getattr(self, "Config", None)
- if is_config_class(config_class):
- return getattr(
- self.core.config.rule,
- config_class.__config_name__,
- default,
- )
- return default
-
- @final
- def stop(self) -> NoReturn:
- """Stop propagation of current events."""
- raise StopException
-
- @final
- def skip(self) -> NoReturn:
- """Skips itself and continues propagation of the current event."""
- raise SkipException
-
- @property
- def state(self) -> StateT:
- """rule status."""
- return self.core.rule_state[self.name]
-
- @state.setter
- @final
- def state(self, value: StateT) -> None:
- self.core.rule_state[self.name] = value
-
- async def enable(self): ...
-
- async def disable(self): ...
-
- @staticmethod
- def aliases(names, ignore_case=False):
- def decorator(func):
- func._aliases = names
- func._ignore_case = ignore_case
- return func
-
- return decorator
-
- @final
- async def safe_run(self) -> None:
- try:
- await self.enable()
- except Exception as e:
- self.bot.error_or_exception(
- f"Enable rule {self.__class__.__name__} failed:", e
- )
diff --git a/hrc/service/__init__.py b/hrc/service/__init__.py
deleted file mode 100644
index f153d5b..0000000
--- a/hrc/service/__init__.py
+++ /dev/null
@@ -1,111 +0,0 @@
-import os
-from abc import ABC, abstractmethod
-from typing import (
- TYPE_CHECKING,
- Any,
- Awaitable,
- Callable,
- Generic,
- Optional,
- Type,
- TypeVar,
- Union,
- final,
- overload,
-)
-
-from hrc.event import Event
-from hrc.typing import ConfigT, EventT
-from hrc.utils import is_config_class
-
-if TYPE_CHECKING:
- from ..core import Core
-
-__all__ = ["Server"]
-
-if os.getenv("IAMAI_DEV") == "1": # pragma: no cover
- # 当处于开发环境时,使用 pkg_resources 风格的命名空间包
- __import__("pkg_resources").declare_namespace(__name__)
-
-
-_EventT = TypeVar("_EventT", bound="Event[Any]")
-
-
-class Service(Generic[EventT, ConfigT], ABC):
- name: str
- core: "Core"
- Config: Type[ConfigT]
-
- def __init__(self, core: "Core") -> None:
- if not hasattr(self, "name"):
- self.name = self.__class__.__name__
- self.core: Core = core
- self.handle_event = self.core.handle_event
-
- @property
- def config(self) -> ConfigT:
- default: Any = None
- config_class = getattr(self, "Config", None)
- if is_config_class(config_class):
- return getattr(
- self.core.config.service,
- config_class.__config_name__,
- default,
- )
- return default
-
- @final
- async def safe_run(self) -> None:
- try:
- await self.run()
- except Exception as e:
- self.core.error_or_exception(
- f"Run service {self.__class__.__name__} failed:", e
- )
-
- @abstractmethod
- async def run(self) -> None:
- raise NotImplementedError
-
- async def startup(self) -> None:
- ...
-
- async def shutdown(self) -> None:
- ...
-
- @overload
- async def get(
- self,
- func: Optional[Callable[[EventT], Union[bool, Awaitable[bool]]]] = None,
- *,
- event_type: None = None,
- max_try_times: Optional[int] = None,
- timeout: Optional[Union[int, float]] = None,
- ) -> EventT: ...
-
- @overload
- async def get(
- self,
- func: Optional[Callable[[_EventT], Union[bool, Awaitable[bool]]]] = None,
- *,
- event_type: Type[_EventT],
- max_try_times: Optional[int] = None,
- timeout: Optional[Union[int, float]] = None,
- ) -> _EventT: ...
-
- @final
- async def get(
- self,
- func: Optional[Callable[[Any], Union[bool, Awaitable[bool]]]] = None,
- *,
- event_type: Any = None,
- max_try_times: Optional[int] = None,
- timeout: Optional[Union[int, float]] = None,
- ) -> Event[Any]:
- return await self.core.get(
- func,
- event_type=event_type,
- server_type=type(self),
- max_try_times=max_try_times,
- timeout=timeout,
- ) \ No newline at end of file
diff --git a/hrc/service/console/__init__.py b/hrc/service/console/__init__.py
deleted file mode 100644
index e11768e..0000000
--- a/hrc/service/console/__init__.py
+++ /dev/null
@@ -1,41 +0,0 @@
-import asyncio
-import sys
-from typing_extensions import override
-
-from hrc.event import MessageEvent
-from hrc.service import Service
-
-class ConsoleServiceEvent(MessageEvent["ConsoleService"]):
- message: str
-
- @override
- def get_sender_id(self) -> None:
- return None
-
- @override
- def get_plain_text(self) -> str:
- return self.message
-
- @override
- async def reply(self, message: str) -> None:
- return await self.service.send(message)
-
- async def is_same_sender(self) -> bool:
- return True
-
-class ConsoleService(Service[ConsoleServiceEvent, None]):
- name: str = "console"
-
- @override
- async def run(self) -> None:
- while not self.core.should_exit.is_set():
- print("Please input message: ") # noqa: T201
- message = await asyncio.get_event_loop().run_in_executor(
- None, sys.stdin.readline
- )
- await self.handle_event(
- ConsoleServiceEvent(service=self, type="message", message=message, rule="")
- )
-
- async def send(self, message: str) -> None:
- print(f"Send a message: {message}") # noqa: T201 \ No newline at end of file
diff --git a/hrc/service/http/__init__.py b/hrc/service/http/__init__.py
deleted file mode 100644
index a8d938b..0000000
--- a/hrc/service/http/__init__.py
+++ /dev/null
@@ -1,33 +0,0 @@
-from typing_extensions import override
-
-from aiohttp import web
-
-from hrc.service.utils import HttpServerService
-from hrc.event import Event
-from hrc.log import logger
-from aiohttp import web
-
-class HttpServerTestEvent(Event["HttpServerTestService"]):
- """HTTP 服务端示例适配器事件类。"""
-
- message: str
-
-
-class HttpServerTestService(HttpServerService[HttpServerTestEvent, None]):
- name: str = "http_server_service"
- get_url: str = "/"
- post_url: str = "/"
- host: str = "127.0.0.1"
- port: int = 8080
-
-
- @override
- async def handle_response(self, request: web.Request) -> web.StreamResponse:
- event = HttpServerTestEvent(
- service=self,
- type="message",
- rule="",
- message=await request.text(),
- )
- await self.handle_event(event)
- return web.Response() \ No newline at end of file
diff --git a/hrc/service/utils.py b/hrc/service/utils.py
deleted file mode 100644
index 4f29e0c..0000000
--- a/hrc/service/utils.py
+++ /dev/null
@@ -1,256 +0,0 @@
-import asyncio
-from abc import ABCMeta, abstractmethod
-from typing import Literal, Optional, Union
-
-import aiohttp
-from aiohttp import web
-
-from hrc.service import Service
-from hrc.log import logger
-from hrc.typing import ConfigT, EventT
-
-__all__ = [
- "PollingService",
- "HttpClientService",
- "WebSocketClientService",
- "HttpServerService",
- "WebSocketServerService",
- "WebSocketService",
-]
-
-
-class PollingService(Service[EventT, ConfigT], metaclass=ABCMeta):
- """轮询式适配器示例。"""
-
- delay: float = 0.1
- create_task: bool = False
- _on_tick_task: Optional["asyncio.Task[None]"] = None
-
- async def run(self) -> None:
- while not self.core.should_exit.is_set():
- await asyncio.sleep(self.delay)
- if self.create_task:
- self._on_tick_task = asyncio.create_task(self.on_tick())
- else:
- await self.on_tick()
-
- @abstractmethod
- async def on_tick(self) -> None:
- """当轮询发生。"""
-
-
-class HttpClientService(PollingService[EventT, ConfigT], metaclass=ABCMeta):
- session: aiohttp.ClientSession
-
- async def startup(self) -> None:
- self.session = aiohttp.ClientSession()
-
- @abstractmethod
- async def on_tick(self) -> None:
- ...
- async def shutdown(self) -> None:
- """关闭并清理连接。"""
- await self.session.close()
-
-
-class WebSocketClientService(Service[EventT, ConfigT], metaclass=ABCMeta):
- url: str
-
- async def run(self) -> None:
- async with aiohttp.ClientSession() as session, session.ws_connect(
- self.url
- ) as ws:
- msg: aiohttp.WSMessage
- async for msg in ws:
- if self.core.should_exit.is_set():
- break
- if msg.type == aiohttp.WSMsgType.ERROR:
- break
- await self.handle_response(msg)
-
- @abstractmethod
- async def handle_response(self, msg: aiohttp.WSMessage) -> None:
- """处理响应。"""
-
-
-class HttpServerService(Service[EventT, ConfigT], metaclass=ABCMeta):
- app: web.Application
- runner: web.AppRunner
- site: web.TCPSite
- host: str
- port: int
- get_url: str
- post_url: str
-
- async def startup(self) -> None:
- """初始化适配器。"""
- self.app = web.Application()
- self.app.add_routes(
- [
- web.get(self.get_url, self.handle_response),
- web.post(self.post_url, self.handle_response),
- ]
- )
-
- async def run(self) -> None:
- self.runner = web.AppRunner(self.app)
- await self.runner.setup()
- self.site = web.TCPSite(self.runner, self.host, self.port)
- await self.site.start()
-
- async def shutdown(self) -> None:
- """关闭并清理连接。"""
- await self.runner.cleanup()
-
- @abstractmethod
- async def handle_response(self, request: web.Request) -> web.StreamResponse:
- """处理响应。"""
-
-
-class WebSocketServerService(Service[EventT, ConfigT], metaclass=ABCMeta):
- app: web.Application
- runner: web.AppRunner
- site: web.TCPSite
- websocket: web.WebSocketResponse
- host: str
- port: int
- url: str
-
- async def startup(self) -> None:
- self.app = web.Application()
- self.app.add_routes([web.get(self.url, self.handle_response)])
-
- async def run(self) -> None:
- self.runner = web.AppRunner(self.app)
- await self.runner.setup()
- self.site = web.TCPSite(self.runner, self.host, self.port)
- await self.site.start()
-
- async def shutdown(self) -> None:
- """关闭并清理连接。"""
- await self.websocket.close()
- await self.site.stop()
- await self.runner.cleanup()
-
- async def handle_response(self, request: web.Request) -> web.WebSocketResponse:
- """处理 WebSocket。"""
- ws = web.WebSocketResponse()
- await ws.prepare(request)
- self.websocket = ws
-
- msg: aiohttp.WSMessage
- async for msg in ws:
- if msg.type == aiohttp.WSMsgType.TEXT:
- await self.handle_ws_response(msg)
- elif msg.type == aiohttp.WSMsgType.ERROR:
- break
-
- return ws
-
- @abstractmethod
- async def handle_ws_response(self, msg: aiohttp.WSMessage) -> None:
- """处理 WebSocket 响应。"""
-
-
-class WebSocketService(Service[EventT, ConfigT], metaclass=ABCMeta):
- """
- 同时支持 WebSocket 客户端和服务端。
- """
-
- websocket: Union[web.WebSocketResponse, aiohttp.ClientWebSocketResponse, None] = (
- None
- )
-
- # ws
- session: Optional[aiohttp.ClientSession]
-
- # reverse-ws
- app: Optional[web.Application]
- runner: Optional[web.AppRunner]
- site: Optional[web.TCPSite]
-
- # config
- service_type: Literal["ws", "reverse-ws"]
- host: str
- port: int
- url: str
- reconnect_interval: int = 3
-
- async def startup(self) -> None:
- if self.service_type == "ws":
- self.session = aiohttp.ClientSession()
- elif self.service_type == "reverse-ws":
- self.app = web.Application()
- self.app.add_routes([web.get(self.url, self.handle_reverse_ws_response)])
- else:
- logger.error(
- 'Config "service_type" must be "ws" or "reverse-ws", not '
- + self.service_type
- )
-
- async def run(self) -> None:
- if self.service_type == "ws":
- while True:
- try:
- await self.websocket_connect()
- except aiohttp.ClientError as e:
- self.core.error_or_exception("WebSocket connection error:", e)
- if self.core.should_exit.is_set():
- break
- await asyncio.sleep(self.reconnect_interval)
- elif self.service_type == "reverse-ws":
- assert self.app is not None
- self.runner = web.AppRunner(self.app)
- await self.runner.setup()
- self.site = web.TCPSite(self.runner, self.host, self.port)
- await self.site.start()
-
- async def shutdown(self) -> None:
- """关闭并清理连接。"""
- if self.websocket is not None:
- await self.websocket.close()
- if self.service_type == "ws":
- if self.session is not None:
- await self.session.close()
- elif self.service_type == "reverse-ws":
- if self.site is not None:
- await self.site.stop()
- if self.runner is not None:
- await self.runner.cleanup()
-
- async def handle_reverse_ws_response(
- self, request: web.Request
- ) -> web.WebSocketResponse:
- """处理 aiohttp WebSocket 服务器的接收。"""
- self.websocket = web.WebSocketResponse()
- await self.websocket.prepare(request)
- await self.reverse_ws_connection_hook()
- await self.handle_websocket()
- return self.websocket
-
- async def reverse_ws_connection_hook(self) -> None:
- """反向 WebSocket 连接建立时的钩子函数。"""
- logger.info("WebSocket connected!")
-
- async def websocket_connect(self) -> None:
- """创建正向 WebSocket 连接。"""
- assert self.session is not None
- logger.info("Tying to connect to WebSocket server...")
- async with self.session.ws_connect(
- f"ws://{self.host}:{self.port}{self.url}"
- ) as self.websocket:
- await self.handle_websocket()
-
- async def handle_websocket(self) -> None:
- """处理 WebSocket。"""
- if self.websocket is None or self.websocket.closed:
- return
- async for msg in self.websocket:
- await self.handle_websocket_msg(msg)
- if not self.core.should_exit.is_set():
- logger.warning("WebSocket connection closed!")
-
- @abstractmethod
- async def handle_websocket_msg(self, msg: aiohttp.WSMessage) -> None:
- """处理 WebSocket 消息。"""
- raise NotImplementedError \ No newline at end of file
diff --git a/hrc/service/websocket/__init__.py b/hrc/service/websocket/__init__.py
deleted file mode 100644
index 3a7f089..0000000
--- a/hrc/service/websocket/__init__.py
+++ /dev/null
@@ -1,30 +0,0 @@
-from typing import Any, Coroutine
-from typing_extensions import override
-from aiohttp import web, ClientWebSocketResponse
-
-from hrc.service.utils import WebSocketService
-from hrc.event import Event
-from hrc.log import logger
-
-from aiohttp import web
-
-class WebSocketTestEvent(Event["WebSocketTestEvent"]):
- message: str
-
-class WebSocketTestService(WebSocketService[WebSocketTestEvent, None]):
- name: str = "websocket_test_service"
- service_type: str = "reverse-ws"
- host: str = "127.0.0.1"
- port: int = 8765
- url: str = "/"
-
- @override
- async def handle_reverse_ws_response(self, request: web.Request) -> Coroutine[Any, Any, ClientWebSocketResponse]:
- event = WebSocketTestEvent(
- service=self,
- type="message",
- message=await request.text()
- )
- logger.info(f"Receive {event}")
- await self.handle_event(event)
- return web.Response() \ No newline at end of file
diff --git a/hrc/typing.py b/hrc/typing.py
deleted file mode 100644
index a207c80..0000000
--- a/hrc/typing.py
+++ /dev/null
@@ -1,23 +0,0 @@
-# ruff: noqa: TCH001
-from typing import TYPE_CHECKING, Awaitable, Callable, Optional, TypeVar
-
-if TYPE_CHECKING:
- from typing import Any
-
- from hrc.service import Service
- from hrc.core import Core
- from hrc.config import ConfigModel
- from hrc.event import Event
- from hrc.rule import Rule
-
-
-StateT = TypeVar("StateT")
-EventT = TypeVar("EventT", bound="Event[Any]")
-RuleT = TypeVar("RuleT", bound="Rule[Any, Any, Any]")
-ConfigT = TypeVar("ConfigT", bound=Optional["ConfigModel"])
-ServiceT = TypeVar("ServiceT", bound="Service[Any, Any]")
-
-CoreHook = Callable[["Core"], Awaitable[None]]
-RuleHook = Callable[["Rule"], Awaitable[None]]
-ServiceHook = Callable[["Service[Any, Any]"], Awaitable[None]]
-EventHook = Callable[["Event[Any]"], Awaitable[None]]
diff --git a/hrc/utils.py b/hrc/utils.py
deleted file mode 100644
index e1399f5..0000000
--- a/hrc/utils.py
+++ /dev/null
@@ -1,291 +0,0 @@
-"""A utility used internally by iamai."""
-
-import asyncio
-import importlib
-import inspect
-import json
-import os
-import os.path
-import sys
-import traceback
-from abc import ABC
-from contextlib import asynccontextmanager
-from functools import partial
-from importlib.abc import MetaPathFinder
-from importlib.machinery import ModuleSpec, PathFinder
-from types import GetSetDescriptorType, ModuleType
-from typing import (
- TYPE_CHECKING,
- Any,
- AsyncGenerator,
- Awaitable,
- Callable,
- ClassVar,
- ContextManager,
- Coroutine,
- Dict,
- List,
- Optional,
- Sequence,
- Tuple,
- Type,
- TypeVar,
- Union,
- cast,
-)
-from typing_extensions import ParamSpec, TypeAlias, TypeGuard
-
-from pydantic import BaseModel
-
-from hrc.config import ConfigModel
-from hrc.typing import EventT
-
-if TYPE_CHECKING:
- from os import PathLike
-
-__all__ = [
- "ModulePathFinder",
- "is_config_class",
- "get_classes_from_module",
- "get_classes_from_module_name",
- "PydanticEncoder",
- "samefile",
- "sync_func_wrapper",
- "sync_ctx_manager_wrapper",
- "wrap_get_func",
- "get_annotations",
-]
-
-_T = TypeVar("_T")
-_P = ParamSpec("_P")
-_R = TypeVar("_R")
-_TypeT = TypeVar("_TypeT", bound=Type[Any])
-
-StrOrBytesPath: TypeAlias = Union[str, bytes, "PathLike[str]", "PathLike[bytes]"]
-
-
-class ModulePathFinder(MetaPathFinder):
- """Meta path finder for finding iamai components."""
-
- path: ClassVar[List[str]] = []
-
- def find_spec(
- self,
- fullname: str,
- path: Optional[Sequence[str]] = None,
- target: Optional[ModuleType] = None,
- ) -> Union[ModuleSpec, None]:
- """Used to find the ``spec`` of a specified module."""
- if path is None:
- path = []
- return PathFinder.find_spec(fullname, self.path + list(path), target)
-
-
-def is_config_class(config_class: Any) -> TypeGuard[Type[ConfigModel]]:
- return (
- inspect.isclass(config_class)
- and issubclass(config_class, ConfigModel)
- and isinstance(getattr(config_class, "__config_name__", None), str)
- and ABC not in config_class.__bases__
- and not inspect.isabstract(config_class)
- )
-
-
-def get_classes_from_module(module: ModuleType, super_class: _TypeT) -> List[_TypeT]:
- """Find a class of the specified type from the module.
-
- Args:
- module: Python module.
- super_class: The superclass of the class to be found.
-
- Returns:
- Returns a list of classes that meet the criteria.
- """
- classes: List[_TypeT] = []
- for _, module_attr in inspect.getmembers(module, inspect.isclass):
- if (
- (inspect.getmodule(module_attr) or module) is module
- and issubclass(module_attr, super_class)
- and module_attr != super_class
- and ABC not in module_attr.__bases__
- and not inspect.isabstract(module_attr)
- ):
- classes.append(cast(_TypeT, module_attr))
- return classes
-
-
-def get_classes_from_module_name(
- name: str, super_class: _TypeT, *, reload: bool = False
-) -> List[Tuple[_TypeT, ModuleType]]:
- """Find a class of the specified type from the module with the specified name.
-
- Args:
- name: module name, the format is the same as the Python ``import`` statement.
- super_class: The superclass of the class to be found.
- reload: Whether to reload the module.
-
- Returns:
- Returns a list of tuples consisting of classes and modules that meet the criteria.
-
- Raises:
- ImportError: An error occurred while importing the module.
- """
- try:
- importlib.invalidate_caches()
- module = importlib.import_module(name)
- if reload:
- importlib.reload(module)
- return [(x, module) for x in get_classes_from_module(module, super_class)]
- except KeyboardInterrupt:
- # Do not capture KeyboardInterrupt
- # Catching KeyboardInterrupt will prevent the user from closing Python when the module being imported is stuck in an infinite loop
- raise
- except BaseException as e:
- raise ImportError(e, traceback.format_exc()) from e
-
-
-class PydanticEncoder(json.JSONEncoder):
- """``JSONEncoder`` class for parsing ``pydantic.BaseModel``."""
-
- def default(self, o: Any) -> Any:
- """Returns a serializable object of ``o``."""
- if isinstance(o, BaseModel):
- return o.model_dump(mode="json")
- return super().default(o)
-
-
-def samefile(path1: StrOrBytesPath, path2: StrOrBytesPath) -> bool:
- """A simple wrapper around ``os.path.samefile``.
-
- Args:
- path1: path1.
- path2: path 2.
-
- Returns:
- If two paths point to the same file or directory.
- """
- try:
- return path1 == path2 or os.path.samefile(path1, path2) # noqa: PTH121
- except OSError:
- return False
-
-
-def sync_func_wrapper(
- func: Callable[_P, _R], *, to_thread: bool = False
-) -> Callable[_P, Coroutine[None, None, _R]]:
- """Wrap a synchronous function as an asynchronous function.
-
- Args:
- func: synchronous function to be packaged.
- to_thread: Whether to run the synchronization function in a separate thread. Defaults to ``False``.
-
- Returns:
- Asynchronous functions.
- """
- if to_thread:
-
- async def _wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R:
- loop = asyncio.get_running_loop()
- func_call = partial(func, *args, **kwargs)
- return await loop.run_in_executor(None, func_call)
-
- else:
-
- async def _wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R:
- return func(*args, **kwargs)
-
- return _wrapper
-
-
-@asynccontextmanager
-async def sync_ctx_manager_wrapper(
- cm: ContextManager[_T], *, to_thread: bool = False
-) -> AsyncGenerator[_T, None]:
- """Wrap a synchronous context manager into an asynchronous context manager.
-
- Args:
- cm: The synchronization context manager to be wrapped.
- to_thread: Whether to run the synchronization function in a separate thread. Defaults to ``False``.
-
- Returns:
- Asynchronous context manager.
- """
- try:
- yield await sync_func_wrapper(cm.__enter__, to_thread=to_thread)()
- except Exception as e:
- if not await sync_func_wrapper(cm.__exit__, to_thread=to_thread)(
- type(e), e, e.__traceback__
- ):
- raise
- else:
- await sync_func_wrapper(cm.__exit__, to_thread=to_thread)(None, None, None)
-
-
-def wrap_get_func(
- func: Optional[Callable[[EventT], Union[bool, Awaitable[bool]]]],
-) -> Callable[[EventT], Awaitable[bool]]:
- """Wrap the parameters accepted by the ``get()`` function into an asynchronous function.
-
- Args:
- func: The parameters accepted by the ``get()`` function.
-
- Returns:
- Asynchronous functions.
- """
- if func is None:
- return sync_func_wrapper(lambda _: True)
- if not asyncio.iscoroutinefunction(func):
- return sync_func_wrapper(func) # type: ignore
- return func
-
-
-if sys.version_info >= (3, 10): # pragma: no cover
- from inspect import get_annotations
-else: # pragma: no cover
-
- def get_annotations(
- obj: Union[Callable[..., object], Type[Any], ModuleType],
- ) -> Dict[str, Any]:
- """Compute the annotation dictionary of an object.
-
- Args:
- obj: A callable object, class, or module.
-
- Raises:
- TypeError: ``obj`` is not a callable object, class or module.
- ValueError: Object's ``__annotations__`` is not a dictionary or ``None``.
-
- Returns:
- Annotation dictionary for objects.
- """
- ann: Union[Dict[str, Any], None]
-
- if isinstance(obj, type):
- # class
- obj_dict = getattr(obj, "__dict__", None)
- if obj_dict and hasattr(obj_dict, "get"):
- ann = obj_dict.get("__annotations__", None)
- if isinstance(ann, GetSetDescriptorType):
- ann = None
- else:
- ann = None
- elif isinstance(obj, ModuleType) or callable(obj):
- # this includes types.ModuleType, types.Function, types.BuiltinFunctionType,
- # types.BuiltinMethodType, functools.partial, functools.singledispatch,
- # "class funclike" from Lib/test/test_inspect... on and on it goes.
- ann = getattr(obj, "__annotations__", None)
- else:
- raise TypeError(f"{obj!r} is not a module, class, or callable.")
-
- if ann is None:
- return {}
-
- if not isinstance(ann, dict):
- raise ValueError( # noqa: TRY004
- f"{obj!r}.__annotations__ is neither a dict nor None"
- )
-
- if not ann:
- return {}
-
- return dict(ann)