diff options
Diffstat (limited to 'hrc')
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) |
