aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/hrc/__init__.py1
-rwxr-xr-xsrc/hrc/_core.abi3.sobin0 -> 7887512 bytes
-rw-r--r--src/hrc/_core.pyi4
-rw-r--r--src/hrc/cli.py46
-rw-r--r--src/hrc/config.py34
-rw-r--r--src/hrc/const.py0
-rw-r--r--src/hrc/core.py0
-rw-r--r--src/hrc/dependencies.py115
-rw-r--r--src/hrc/dev/__init__.py1
-rw-r--r--src/hrc/dev/api/__init__.py0
-rw-r--r--src/hrc/dev/character.py2
-rw-r--r--src/hrc/dev/echo.py105
-rw-r--r--src/hrc/dev/grps/__init__.py1
-rw-r--r--src/hrc/dev/grps/v1.py12
-rw-r--r--src/hrc/doc/__init__.py1
-rw-r--r--src/hrc/event.py94
-rw-r--r--src/hrc/exceptions.py22
-rw-r--r--src/hrc/feat/__init__.py0
-rw-r--r--src/hrc/log.py22
-rw-r--r--src/hrc/perf/__init__.py0
-rw-r--r--src/hrc/py.typed0
-rw-r--r--src/hrc/rule/BaseRule/CharacterCard.py17
-rw-r--r--src/hrc/rule/BaseRule/CustomRule.py0
-rw-r--r--src/hrc/rule/BaseRule/Wiki.py0
-rw-r--r--src/hrc/rule/BaseRule/__init__.py3
-rw-r--r--src/hrc/rule/__init__.py164
-rw-r--r--src/hrc/service/__init__.py111
-rw-r--r--src/hrc/service/console/__init__.py41
-rw-r--r--src/hrc/service/http/__init__.py33
-rw-r--r--src/hrc/service/utils.py256
-rw-r--r--src/hrc/service/websocket/__init__.py30
-rw-r--r--src/hrc/typing.py23
-rw-r--r--src/hrc/utils.py291
-rw-r--r--src/lib.rs29
34 files changed, 1446 insertions, 12 deletions
diff --git a/src/hrc/__init__.py b/src/hrc/__init__.py
new file mode 100644
index 0000000..4876897
--- /dev/null
+++ b/src/hrc/__init__.py
@@ -0,0 +1 @@
+from ._core import * # noqa: F403 \ No newline at end of file
diff --git a/src/hrc/_core.abi3.so b/src/hrc/_core.abi3.so
new file mode 100755
index 0000000..2d85fa5
--- /dev/null
+++ b/src/hrc/_core.abi3.so
Binary files differ
diff --git a/src/hrc/_core.pyi b/src/hrc/_core.pyi
new file mode 100644
index 0000000..78ae466
--- /dev/null
+++ b/src/hrc/_core.pyi
@@ -0,0 +1,4 @@
+class LibCore(object):
+ """Core library for hydro roll"""
+
+ def __init__(self, name: str = ""): ... \ No newline at end of file
diff --git a/src/hrc/cli.py b/src/hrc/cli.py
new file mode 100644
index 0000000..55758bc
--- /dev/null
+++ b/src/hrc/cli.py
@@ -0,0 +1,46 @@
+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/src/hrc/config.py b/src/hrc/config.py
new file mode 100644
index 0000000..d179258
--- /dev/null
+++ b/src/hrc/config.py
@@ -0,0 +1,34 @@
+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/src/hrc/const.py b/src/hrc/const.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/hrc/const.py
diff --git a/src/hrc/core.py b/src/hrc/core.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/hrc/core.py
diff --git a/src/hrc/dependencies.py b/src/hrc/dependencies.py
new file mode 100644
index 0000000..e176e14
--- /dev/null
+++ b/src/hrc/dependencies.py
@@ -0,0 +1,115 @@
+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/src/hrc/dev/__init__.py b/src/hrc/dev/__init__.py
new file mode 100644
index 0000000..ea8f56b
--- /dev/null
+++ b/src/hrc/dev/__init__.py
@@ -0,0 +1 @@
+from hrc.dev.grps import v1 \ No newline at end of file
diff --git a/src/hrc/dev/api/__init__.py b/src/hrc/dev/api/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/hrc/dev/api/__init__.py
diff --git a/src/hrc/dev/character.py b/src/hrc/dev/character.py
new file mode 100644
index 0000000..c883e45
--- /dev/null
+++ b/src/hrc/dev/character.py
@@ -0,0 +1,2 @@
+class Character:
+ class Attribute: ...
diff --git a/src/hrc/dev/echo.py b/src/hrc/dev/echo.py
new file mode 100644
index 0000000..5bdeab7
--- /dev/null
+++ b/src/hrc/dev/echo.py
@@ -0,0 +1,105 @@
+"""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/src/hrc/dev/grps/__init__.py b/src/hrc/dev/grps/__init__.py
new file mode 100644
index 0000000..bbf8c7e
--- /dev/null
+++ b/src/hrc/dev/grps/__init__.py
@@ -0,0 +1 @@
+from . import v1 \ No newline at end of file
diff --git a/src/hrc/dev/grps/v1.py b/src/hrc/dev/grps/v1.py
new file mode 100644
index 0000000..fa987a7
--- /dev/null
+++ b/src/hrc/dev/grps/v1.py
@@ -0,0 +1,12 @@
+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/src/hrc/doc/__init__.py b/src/hrc/doc/__init__.py
new file mode 100644
index 0000000..998b999
--- /dev/null
+++ b/src/hrc/doc/__init__.py
@@ -0,0 +1 @@
+import sphinx \ No newline at end of file
diff --git a/src/hrc/event.py b/src/hrc/event.py
new file mode 100644
index 0000000..961a356
--- /dev/null
+++ b/src/hrc/event.py
@@ -0,0 +1,94 @@
+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/src/hrc/exceptions.py b/src/hrc/exceptions.py
new file mode 100644
index 0000000..c71118f
--- /dev/null
+++ b/src/hrc/exceptions.py
@@ -0,0 +1,22 @@
+class EventException(BaseException):
+ ...
+
+
+class SkipException(EventException):
+ ...
+
+
+class StopException(EventException):
+ ...
+
+
+class CoreException(Exception):
+ ... # noqa: N818
+
+
+class GetEventTimeout(CoreException):
+ ...
+
+
+class LoadModuleError(CoreException):
+ ...
diff --git a/src/hrc/feat/__init__.py b/src/hrc/feat/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/hrc/feat/__init__.py
diff --git a/src/hrc/log.py b/src/hrc/log.py
new file mode 100644
index 0000000..8e476a6
--- /dev/null
+++ b/src/hrc/log.py
@@ -0,0 +1,22 @@
+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/src/hrc/perf/__init__.py b/src/hrc/perf/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/hrc/perf/__init__.py
diff --git a/src/hrc/py.typed b/src/hrc/py.typed
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/hrc/py.typed
diff --git a/src/hrc/rule/BaseRule/CharacterCard.py b/src/hrc/rule/BaseRule/CharacterCard.py
new file mode 100644
index 0000000..2baea48
--- /dev/null
+++ b/src/hrc/rule/BaseRule/CharacterCard.py
@@ -0,0 +1,17 @@
+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/src/hrc/rule/BaseRule/CustomRule.py b/src/hrc/rule/BaseRule/CustomRule.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/hrc/rule/BaseRule/CustomRule.py
diff --git a/src/hrc/rule/BaseRule/Wiki.py b/src/hrc/rule/BaseRule/Wiki.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/hrc/rule/BaseRule/Wiki.py
diff --git a/src/hrc/rule/BaseRule/__init__.py b/src/hrc/rule/BaseRule/__init__.py
new file mode 100644
index 0000000..fdde86c
--- /dev/null
+++ b/src/hrc/rule/BaseRule/__init__.py
@@ -0,0 +1,3 @@
+from . import CharacterCard # noqa: F401
+from . import CustomRule # noqa: F401
+from . import Wiki # noqa: F401
diff --git a/src/hrc/rule/__init__.py b/src/hrc/rule/__init__.py
new file mode 100644
index 0000000..282e3e2
--- /dev/null
+++ b/src/hrc/rule/__init__.py
@@ -0,0 +1,164 @@
+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/src/hrc/service/__init__.py b/src/hrc/service/__init__.py
new file mode 100644
index 0000000..f153d5b
--- /dev/null
+++ b/src/hrc/service/__init__.py
@@ -0,0 +1,111 @@
+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/src/hrc/service/console/__init__.py b/src/hrc/service/console/__init__.py
new file mode 100644
index 0000000..e11768e
--- /dev/null
+++ b/src/hrc/service/console/__init__.py
@@ -0,0 +1,41 @@
+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/src/hrc/service/http/__init__.py b/src/hrc/service/http/__init__.py
new file mode 100644
index 0000000..a8d938b
--- /dev/null
+++ b/src/hrc/service/http/__init__.py
@@ -0,0 +1,33 @@
+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/src/hrc/service/utils.py b/src/hrc/service/utils.py
new file mode 100644
index 0000000..4f29e0c
--- /dev/null
+++ b/src/hrc/service/utils.py
@@ -0,0 +1,256 @@
+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/src/hrc/service/websocket/__init__.py b/src/hrc/service/websocket/__init__.py
new file mode 100644
index 0000000..3a7f089
--- /dev/null
+++ b/src/hrc/service/websocket/__init__.py
@@ -0,0 +1,30 @@
+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/src/hrc/typing.py b/src/hrc/typing.py
new file mode 100644
index 0000000..a207c80
--- /dev/null
+++ b/src/hrc/typing.py
@@ -0,0 +1,23 @@
+# 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/src/hrc/utils.py b/src/hrc/utils.py
new file mode 100644
index 0000000..e1399f5
--- /dev/null
+++ b/src/hrc/utils.py
@@ -0,0 +1,291 @@
+"""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)
diff --git a/src/lib.rs b/src/lib.rs
index 2c628ba..dc6e6d0 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,19 +1,24 @@
use pyo3::prelude::*;
-use pyo3::wrap_pyfunction;
#[pyfunction]
-fn process_rule_pack(rule_pack: &str) -> PyResult<String> {
- Ok(format!("Processed rule pack: {}", rule_pack))
+fn sum_as_string(a: usize, b: usize) -> PyResult<String> {
+ Ok((a + b).to_string())
}
-#[pymodule]
-#[pyo3(name = "LibCore")]
-fn libcore(_py: Python, m: &PyModule) -> PyResult<()> {
- let _py_hrc_log = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/hrc/log.py"));
- let _py_hrc_event = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/hrc/event.py"));
- let _py_hrc_core = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/hrc/core.py"));
- let _py_hrc_const = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/hrc/const.py"));
+#[pyclass]
+pub struct Base {
+ pub value: i32,
+}
- m.add_function(wrap_pyfunction!(process_rule_pack, m)?)?;
- Ok(())
+impl Base {
+ fn new(value: i32) -> Self {
+ Base { value }
+ }
}
+
+#[pymodule]
+fn _core(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
+ m.add_function(wrap_pyfunction!(sum_as_string, m)?)?;
+ m.add_class::<Base>()?;
+ Ok(())
+} \ No newline at end of file