python-library-automation 0.1.16__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_library_automation-0.1.16 → python_library_automation-0.3.0}/.gitignore +3 -1
- python_library_automation-0.3.0/PKG-INFO +8 -0
- python_library_automation-0.3.0/automation/__init__.py +58 -0
- python_library_automation-0.3.0/automation/assistant.py +113 -0
- {python_library_automation-0.1.16 → python_library_automation-0.3.0}/automation/builtins/__init__.py +1 -1
- python_library_automation-0.3.0/automation/builtins/action/__init__.py +11 -0
- python_library_automation-0.3.0/automation/builtins/action/call_method.py +50 -0
- python_library_automation-0.3.0/automation/builtins/action/delay.py +25 -0
- python_library_automation-0.3.0/automation/builtins/action/log.py +30 -0
- python_library_automation-0.3.0/automation/builtins/action/set_attribute.py +42 -0
- python_library_automation-0.3.0/automation/builtins/event/attribute_change.py +119 -0
- python_library_automation-0.3.0/automation/builtins/event/on_call.py +105 -0
- python_library_automation-0.3.0/automation/core/__init__.py +36 -0
- python_library_automation-0.3.0/automation/core/action.py +143 -0
- python_library_automation-0.3.0/automation/core/base.py +130 -0
- python_library_automation-0.3.0/automation/core/entity.py +271 -0
- python_library_automation-0.3.0/automation/core/event.py +96 -0
- python_library_automation-0.1.16/automation/core/entity.py → python_library_automation-0.3.0/automation/core/info.py +58 -66
- python_library_automation-0.3.0/automation/core/registry_catalog.py +87 -0
- python_library_automation-0.3.0/automation/core/renderer.py +39 -0
- python_library_automation-0.3.0/automation/core/trigger.py +334 -0
- {python_library_automation-0.1.16 → python_library_automation-0.3.0}/automation/errors.py +7 -2
- python_library_automation-0.3.0/automation/listener/__init__.py +41 -0
- python_library_automation-0.3.0/automation/listener/base.py +131 -0
- python_library_automation-0.3.0/automation/listener/console.py +101 -0
- python_library_automation-0.3.0/automation/listener/events.py +147 -0
- python_library_automation-0.3.0/automation/runtime/__init__.py +30 -0
- python_library_automation-0.3.0/automation/runtime/bootstrap.py +227 -0
- python_library_automation-0.3.0/automation/runtime/context.py +168 -0
- python_library_automation-0.3.0/automation/runtime/global_state.py +62 -0
- python_library_automation-0.3.0/automation/runtime/main_flow.py +5 -0
- python_library_automation-0.3.0/automation/runtime/relaunch.py +57 -0
- python_library_automation-0.3.0/debug.bat +14 -0
- {python_library_automation-0.1.16 → python_library_automation-0.3.0}/pyproject.toml +4 -4
- python_library_automation-0.1.16/PKG-INFO +0 -8
- python_library_automation-0.1.16/automation/__init__.py +0 -25
- python_library_automation-0.1.16/automation/assistant.py +0 -141
- python_library_automation-0.1.16/automation/builtins/action/call_entity_method.py +0 -35
- python_library_automation-0.1.16/automation/builtins/action/delay.py +0 -18
- python_library_automation-0.1.16/automation/builtins/action/log.py +0 -20
- python_library_automation-0.1.16/automation/builtins/action/set_attribute.py +0 -27
- python_library_automation-0.1.16/automation/builtins/entity/__init__.py +0 -0
- python_library_automation-0.1.16/automation/builtins/entity/time.py +0 -29
- python_library_automation-0.1.16/automation/builtins/entity/variable.py +0 -81
- python_library_automation-0.1.16/automation/builtins/event/__init__.py +0 -0
- python_library_automation-0.1.16/automation/builtins/event/_scheduled.py +0 -34
- python_library_automation-0.1.16/automation/builtins/event/at.py +0 -27
- python_library_automation-0.1.16/automation/builtins/event/callback.py +0 -65
- python_library_automation-0.1.16/automation/builtins/event/every.py +0 -32
- python_library_automation-0.1.16/automation/builtins/event/state_changed.py +0 -58
- python_library_automation-0.1.16/automation/core/__init__.py +0 -13
- python_library_automation-0.1.16/automation/core/action.py +0 -22
- python_library_automation-0.1.16/automation/core/base.py +0 -59
- python_library_automation-0.1.16/automation/core/composite_action.py +0 -47
- python_library_automation-0.1.16/automation/core/event.py +0 -87
- python_library_automation-0.1.16/automation/core/event_context.py +0 -10
- python_library_automation-0.1.16/automation/core/trigger.py +0 -151
- python_library_automation-0.1.16/automation/executor.py +0 -46
- python_library_automation-0.1.16/automation/hub.py +0 -52
- python_library_automation-0.1.16/automation/listeners/__init__.py +0 -17
- python_library_automation-0.1.16/automation/listeners/base.py +0 -23
- python_library_automation-0.1.16/automation/listeners/console.py +0 -82
- python_library_automation-0.1.16/automation/listeners/instance_schema.py +0 -26
- python_library_automation-0.1.16/automation/listeners/record.py +0 -29
- python_library_automation-0.1.16/automation/listeners/trace.py +0 -70
- python_library_automation-0.1.16/automation/listeners/type_schema.py +0 -26
- python_library_automation-0.1.16/automation/loader.py +0 -131
- python_library_automation-0.1.16/automation/renderer.py +0 -311
- python_library_automation-0.1.16/automation/schema.py +0 -142
- python_library_automation-0.1.16/automation/updater.py +0 -84
- python_library_automation-0.1.16/example-simple.bat +0 -10
- python_library_automation-0.1.16/examples/simple/__main__.py +0 -27
- python_library_automation-0.1.16/tests/support.py +0 -20
- python_library_automation-0.1.16/tests/test_assistant.py +0 -31
- python_library_automation-0.1.16/tests/test_builder.py +0 -43
- python_library_automation-0.1.16/tests/test_expression_condition.py +0 -103
- python_library_automation-0.1.16/tests/test_expression_parser.py +0 -38
- python_library_automation-0.1.16/tests/test_hub.py +0 -20
- python_library_automation-0.1.16/tests/test_schema.py +0 -179
- python_library_automation-0.1.16/tests/test_trigger_flow.py +0 -92
- {python_library_automation-0.1.16/automation/builtins/action → python_library_automation-0.3.0/automation/builtins/event}/__init__.py +0 -0
- {python_library_automation-0.1.16 → python_library_automation-0.3.0}/test.bat +0 -0
- {python_library_automation-0.1.16 → python_library_automation-0.3.0}/update.bat +0 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-library-automation
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Requires-Python: >=3.10
|
|
5
|
+
Requires-Dist: python-library-express-evaluator
|
|
6
|
+
Requires-Dist: python-library-observer
|
|
7
|
+
Requires-Dist: python-library-reactive-model
|
|
8
|
+
Requires-Dist: python-library-registry
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from automation import assistant
|
|
2
|
+
from automation.assistant import (
|
|
3
|
+
add_listener,
|
|
4
|
+
configure_listeners,
|
|
5
|
+
context,
|
|
6
|
+
entities,
|
|
7
|
+
events,
|
|
8
|
+
load_script_file,
|
|
9
|
+
reload_automation,
|
|
10
|
+
run,
|
|
11
|
+
section,
|
|
12
|
+
start,
|
|
13
|
+
stop,
|
|
14
|
+
triggers,
|
|
15
|
+
)
|
|
16
|
+
from automation.core import (
|
|
17
|
+
Action,
|
|
18
|
+
Entity,
|
|
19
|
+
Event,
|
|
20
|
+
Trigger,
|
|
21
|
+
action_registry,
|
|
22
|
+
catalog_registry,
|
|
23
|
+
entity_registry,
|
|
24
|
+
event_registry,
|
|
25
|
+
instantiate_registered,
|
|
26
|
+
registered_kind_for,
|
|
27
|
+
trigger_registry,
|
|
28
|
+
)
|
|
29
|
+
from automation.runtime import Context, get_context
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"Action",
|
|
33
|
+
"Entity",
|
|
34
|
+
"Event",
|
|
35
|
+
"Trigger",
|
|
36
|
+
"Context",
|
|
37
|
+
"assistant",
|
|
38
|
+
"add_listener",
|
|
39
|
+
"configure_listeners",
|
|
40
|
+
"context",
|
|
41
|
+
"entities",
|
|
42
|
+
"events",
|
|
43
|
+
"triggers",
|
|
44
|
+
"section",
|
|
45
|
+
"get_context",
|
|
46
|
+
"load_script_file",
|
|
47
|
+
"reload_automation",
|
|
48
|
+
"run",
|
|
49
|
+
"start",
|
|
50
|
+
"stop",
|
|
51
|
+
"catalog_registry",
|
|
52
|
+
"action_registry",
|
|
53
|
+
"entity_registry",
|
|
54
|
+
"event_registry",
|
|
55
|
+
"trigger_registry",
|
|
56
|
+
"instantiate_registered",
|
|
57
|
+
"registered_kind_for",
|
|
58
|
+
]
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from automation.listener.base import BaseListener
|
|
8
|
+
from automation.listener.events import Loaded, Started, Stopped
|
|
9
|
+
from automation.runtime.bootstrap import load_script, reload_automation, teardown_automation
|
|
10
|
+
from automation.runtime.global_state import (
|
|
11
|
+
RunState,
|
|
12
|
+
add_listener,
|
|
13
|
+
get_context,
|
|
14
|
+
get_main_loop,
|
|
15
|
+
get_run_state,
|
|
16
|
+
set_listeners,
|
|
17
|
+
set_main_loop,
|
|
18
|
+
set_run_state,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def context():
|
|
25
|
+
"""进程内唯一的自动化运行时上下文。"""
|
|
26
|
+
|
|
27
|
+
return get_context()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def entities():
|
|
31
|
+
return get_context().entities
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def events():
|
|
35
|
+
return get_context().events
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def triggers():
|
|
39
|
+
return get_context().triggers
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def section(name: str):
|
|
43
|
+
return get_context().section(name)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def load_script_file(path: str | Path) -> None:
|
|
47
|
+
"""执行自动化脚本并激活其中注册的实体/事件/触发器。"""
|
|
48
|
+
|
|
49
|
+
await load_script(path)
|
|
50
|
+
get_context().emit(Loaded(get_context()))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
async def start() -> None:
|
|
54
|
+
if get_run_state() == RunState.RUNNING:
|
|
55
|
+
return
|
|
56
|
+
set_run_state(RunState.RUNNING)
|
|
57
|
+
ctx = get_context()
|
|
58
|
+
ctx.stop_event.clear()
|
|
59
|
+
set_main_loop(asyncio.get_running_loop())
|
|
60
|
+
ctx.main_loop = get_main_loop()
|
|
61
|
+
for section_name in ctx.AUTOMATION_SECTIONS:
|
|
62
|
+
for obj in ctx.section(section_name).values():
|
|
63
|
+
await obj.run_phase()
|
|
64
|
+
ctx.emit(Started())
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
async def run() -> None:
|
|
68
|
+
await start()
|
|
69
|
+
await get_context().stop_event.wait()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
async def stop() -> None:
|
|
73
|
+
if get_run_state() != RunState.RUNNING:
|
|
74
|
+
return
|
|
75
|
+
set_run_state(RunState.STOPPED)
|
|
76
|
+
ctx = get_context()
|
|
77
|
+
ctx.main_loop = None
|
|
78
|
+
set_main_loop(None)
|
|
79
|
+
for section_name in reversed(ctx.AUTOMATION_SECTIONS):
|
|
80
|
+
for obj in ctx.section(section_name).values():
|
|
81
|
+
await obj.run_phase(closing=True)
|
|
82
|
+
ctx.stop_event.set()
|
|
83
|
+
ctx.emit(Stopped())
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def configure_listeners(
|
|
87
|
+
listeners: list[BaseListener] | BaseListener | None = None,
|
|
88
|
+
) -> None:
|
|
89
|
+
"""登记监听器;须在 load_script 或 reload_automation 之前调用。"""
|
|
90
|
+
|
|
91
|
+
if listeners is None:
|
|
92
|
+
set_listeners([])
|
|
93
|
+
return
|
|
94
|
+
merged = (
|
|
95
|
+
[listeners] if isinstance(listeners, BaseListener) else list(listeners)
|
|
96
|
+
)
|
|
97
|
+
set_listeners(merged)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
__all__ = [
|
|
101
|
+
"context",
|
|
102
|
+
"entities",
|
|
103
|
+
"events",
|
|
104
|
+
"triggers",
|
|
105
|
+
"section",
|
|
106
|
+
"configure_listeners",
|
|
107
|
+
"add_listener",
|
|
108
|
+
"load_script_file",
|
|
109
|
+
"reload_automation",
|
|
110
|
+
"start",
|
|
111
|
+
"run",
|
|
112
|
+
"stop",
|
|
113
|
+
]
|
{python_library_automation-0.1.16 → python_library_automation-0.3.0}/automation/builtins/__init__.py
RENAMED
|
@@ -3,7 +3,7 @@ import pkgutil
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
5
|
def _auto_import():
|
|
6
|
-
"""自动导入 builtins
|
|
6
|
+
"""自动导入 builtins 下所有子模块,完成事件等内置类型的模块级登记。"""
|
|
7
7
|
package_dir = Path(__file__).resolve().parent
|
|
8
8
|
for sub_pkg in package_dir.iterdir():
|
|
9
9
|
if not sub_pkg.is_dir() or sub_pkg.name.startswith("_"):
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from automation.builtins.action.call_method import CallMethodAction
|
|
2
|
+
from automation.builtins.action.delay import DelayAction
|
|
3
|
+
from automation.builtins.action.log import LogAction
|
|
4
|
+
from automation.builtins.action.set_attribute import SetAttributeAction
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"CallMethodAction",
|
|
8
|
+
"DelayAction",
|
|
9
|
+
"LogAction",
|
|
10
|
+
"SetAttributeAction",
|
|
11
|
+
]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pydantic import Field
|
|
7
|
+
|
|
8
|
+
from automation.core.action import Action
|
|
9
|
+
from automation.core.renderer import Renderer
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CallMethodAction(Action):
|
|
13
|
+
"""调用某实体上的方法。"""
|
|
14
|
+
|
|
15
|
+
entity: str = Field(description="实体实例名,对应 context.entities 的键。")
|
|
16
|
+
method: str = Field(description="实体上的方法名。")
|
|
17
|
+
args: dict[str, Any] = Field(
|
|
18
|
+
default_factory=dict,
|
|
19
|
+
description="传给实体方法的命名参数;字符串值在运行期经渲染器求值。",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def display_label(self) -> str:
|
|
24
|
+
return "call_method"
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def log_params(self) -> dict[str, Any]:
|
|
28
|
+
return {"entity": self.entity, "method": self.method, "args": self.args}
|
|
29
|
+
|
|
30
|
+
async def execute(self, renderer: Renderer) -> None:
|
|
31
|
+
entities = self._ctx.entities
|
|
32
|
+
if self.entity not in entities:
|
|
33
|
+
raise ValueError(f"Entity {self.entity!r} not found")
|
|
34
|
+
target = entities[self.entity]
|
|
35
|
+
if not hasattr(target, self.method):
|
|
36
|
+
raise ValueError(
|
|
37
|
+
f"Entity {self.entity!r} has no method {self.method!r}"
|
|
38
|
+
)
|
|
39
|
+
fn = getattr(target, self.method)
|
|
40
|
+
if not callable(fn):
|
|
41
|
+
raise ValueError(
|
|
42
|
+
f"Entity {self.entity!r}.{self.method} is not callable"
|
|
43
|
+
)
|
|
44
|
+
rendered_args = {
|
|
45
|
+
key: renderer(value) if isinstance(value, str) else value
|
|
46
|
+
for key, value in self.args.items()
|
|
47
|
+
}
|
|
48
|
+
result = fn(**rendered_args)
|
|
49
|
+
if inspect.isawaitable(result):
|
|
50
|
+
await result
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
|
|
7
|
+
from automation.core.action import Action
|
|
8
|
+
from automation.core.renderer import Renderer
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DelayAction(Action):
|
|
12
|
+
"""异步等待指定秒数。"""
|
|
13
|
+
|
|
14
|
+
seconds: float = Field(default=0.0, description="等待秒数。")
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def display_label(self) -> str:
|
|
18
|
+
return "delay"
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def log_params(self) -> dict[str, float]:
|
|
22
|
+
return {"seconds": self.seconds}
|
|
23
|
+
|
|
24
|
+
async def execute(self, renderer: Renderer) -> None:
|
|
25
|
+
await asyncio.sleep(self.seconds)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
|
|
7
|
+
from automation.core.action import Action
|
|
8
|
+
from automation.core.renderer import Renderer
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LogAction(Action):
|
|
14
|
+
"""将一条信息写入日志。"""
|
|
15
|
+
|
|
16
|
+
info: str = Field(description="日志正文;字符串值在运行期经表达式渲染器求值。")
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def display_label(self) -> str:
|
|
20
|
+
return "log"
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def log_params(self) -> dict[str, str]:
|
|
24
|
+
return {"info": self.info}
|
|
25
|
+
|
|
26
|
+
async def execute(self, renderer: Renderer) -> None:
|
|
27
|
+
text = self.info
|
|
28
|
+
if "{" in text or "}" in text:
|
|
29
|
+
text = str(renderer(text))
|
|
30
|
+
logger.info(text)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
|
|
7
|
+
from automation.core.action import Action
|
|
8
|
+
from automation.core.renderer import Renderer
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SetAttributeAction(Action):
|
|
12
|
+
"""设置某实体上的属性值。"""
|
|
13
|
+
|
|
14
|
+
entity: str = Field(description="实体实例名,对应 context.entities 的键。")
|
|
15
|
+
attribute: str = Field(description="实体上的属性名。")
|
|
16
|
+
value: Any = Field(description="要写入的值;字符串在运行期经渲染器求值。")
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def display_label(self) -> str:
|
|
20
|
+
return "set_attribute"
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def log_params(self) -> dict[str, Any]:
|
|
24
|
+
return {
|
|
25
|
+
"entity": self.entity,
|
|
26
|
+
"attribute": self.attribute,
|
|
27
|
+
"value": self.value,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async def execute(self, renderer: Renderer) -> None:
|
|
31
|
+
entities = self._ctx.entities
|
|
32
|
+
if self.entity not in entities:
|
|
33
|
+
raise ValueError(f"Entity {self.entity!r} not found")
|
|
34
|
+
target = entities[self.entity]
|
|
35
|
+
if not hasattr(target, self.attribute):
|
|
36
|
+
raise ValueError(
|
|
37
|
+
f"Entity {self.entity!r} has no attribute {self.attribute!r}"
|
|
38
|
+
)
|
|
39
|
+
value = self.value
|
|
40
|
+
if isinstance(value, str):
|
|
41
|
+
value = renderer(value)
|
|
42
|
+
setattr(target, self.attribute, value)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import ConfigDict, Field, PrivateAttr
|
|
6
|
+
|
|
7
|
+
from automation.core.base import BaseAutomation, registered_kind_for
|
|
8
|
+
from automation.core.event import EventContext
|
|
9
|
+
from automation.core.registry_catalog import event_registry
|
|
10
|
+
from automation.core.info import AttributeWatchInfo
|
|
11
|
+
|
|
12
|
+
from .on_call import OnCallEvent
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AttributeChangeEventData(EventContext):
|
|
16
|
+
"""由 on_changes 路径触发:结构化字段与并入条件的扁平 data 同源。"""
|
|
17
|
+
|
|
18
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
19
|
+
|
|
20
|
+
event_name: str = Field(
|
|
21
|
+
default="",
|
|
22
|
+
description="激活 fire 时由 Event 填入本事件实例名;构造监听载荷时可省略",
|
|
23
|
+
)
|
|
24
|
+
entity: BaseAutomation = Field(description="发生写入的实体实例")
|
|
25
|
+
attribute: str = Field(description="被写入的属性名")
|
|
26
|
+
old: Any = Field(description="写入前的取值")
|
|
27
|
+
new: Any = Field(description="写入后的取值")
|
|
28
|
+
|
|
29
|
+
def as_payload(self) -> dict[str, Any]:
|
|
30
|
+
"""转成条件与回调使用的扁平表,实体仍以运行中实例传入。"""
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
"entity": self.entity,
|
|
34
|
+
"attribute": self.attribute,
|
|
35
|
+
"old": self.old,
|
|
36
|
+
"new": self.new,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _register_on_change_handlers(ev: Any) -> None:
|
|
41
|
+
"""按 on_changes 将异步 handler 登记到上下文的属性变更链。"""
|
|
42
|
+
|
|
43
|
+
for h in ev._attribute_watch_handlers:
|
|
44
|
+
try:
|
|
45
|
+
ev._ctx._on_state_changed.remove(h)
|
|
46
|
+
except ValueError:
|
|
47
|
+
pass
|
|
48
|
+
ev._attribute_watch_handlers.clear()
|
|
49
|
+
|
|
50
|
+
for watch in ev.on_changes:
|
|
51
|
+
|
|
52
|
+
async def handler(
|
|
53
|
+
entity: Any,
|
|
54
|
+
attr: str,
|
|
55
|
+
old: Any,
|
|
56
|
+
new: Any,
|
|
57
|
+
*,
|
|
58
|
+
_w: AttributeWatchInfo = watch,
|
|
59
|
+
_ev: Any = ev,
|
|
60
|
+
) -> None:
|
|
61
|
+
if _w.entity_type and registered_kind_for(type(entity)) != _w.entity_type:
|
|
62
|
+
return
|
|
63
|
+
if _w.entity_name and entity.instance_name != _w.entity_name:
|
|
64
|
+
return
|
|
65
|
+
if _w.attribute and attr != _w.attribute:
|
|
66
|
+
return
|
|
67
|
+
await _ev.fire(
|
|
68
|
+
AttributeChangeEventData(
|
|
69
|
+
entity=entity,
|
|
70
|
+
attribute=attr,
|
|
71
|
+
old=old,
|
|
72
|
+
new=new,
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
ev._ctx._on_state_changed.append(handler)
|
|
77
|
+
ev._attribute_watch_handlers.append(handler)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class AttributeWatchEvent(OnCallEvent):
|
|
81
|
+
"""在基类事件之上登记 on_changes,从实体属性变更链触发 fire。"""
|
|
82
|
+
|
|
83
|
+
on_changes: list[AttributeWatchInfo] = Field(
|
|
84
|
+
default_factory=list,
|
|
85
|
+
description="实体属性变更时按条过滤后触发本事件,空列表则不登记属性监听",
|
|
86
|
+
)
|
|
87
|
+
_attribute_watch_handlers: list[Any] = PrivateAttr(default_factory=list)
|
|
88
|
+
|
|
89
|
+
async def on_activate(self) -> None:
|
|
90
|
+
await super().on_activate()
|
|
91
|
+
_register_on_change_handlers(self)
|
|
92
|
+
|
|
93
|
+
async def on_inactive(self) -> None:
|
|
94
|
+
for h in self._attribute_watch_handlers:
|
|
95
|
+
try:
|
|
96
|
+
self._ctx._on_state_changed.remove(h)
|
|
97
|
+
except ValueError:
|
|
98
|
+
pass
|
|
99
|
+
self._attribute_watch_handlers.clear()
|
|
100
|
+
await super().on_inactive()
|
|
101
|
+
|
|
102
|
+
async def fire(
|
|
103
|
+
self,
|
|
104
|
+
data: dict[str, Any] | EventContext | AttributeChangeEventData | None = None,
|
|
105
|
+
) -> None:
|
|
106
|
+
"""在交给基类前将属性变更载荷展开为与条件求值一致的 data。"""
|
|
107
|
+
|
|
108
|
+
if isinstance(data, AttributeChangeEventData):
|
|
109
|
+
data = data.model_copy(
|
|
110
|
+
update={
|
|
111
|
+
"event_name": self.instance_name,
|
|
112
|
+
"data": data.as_payload(),
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
await super().fire(data)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
AttributeWatchEvent.registered_kind = "event"
|
|
119
|
+
event_registry.register("event", AttributeWatchEvent)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from concurrent.futures import Future as ConcurrentFuture
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from pydantic import Field, PrivateAttr
|
|
9
|
+
|
|
10
|
+
from automation.core.base import BaseAutomation, observer_bus
|
|
11
|
+
from automation.core.event import Event
|
|
12
|
+
from automation.core.info import CallInfo
|
|
13
|
+
from automation.core.registry_catalog import event_registry
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class OnCallEvent(Event):
|
|
19
|
+
"""在基类事件之上按规格登记方法调用观测,匹配后触发 fire。"""
|
|
20
|
+
|
|
21
|
+
on_calls: list[CallInfo] = Field(
|
|
22
|
+
default_factory=list,
|
|
23
|
+
description=(
|
|
24
|
+
"与总线发派匹配的多条调用规格,逐项登记;"
|
|
25
|
+
"某条方法名为空则该项不挂总线、仅可依赖其它触发路径"
|
|
26
|
+
),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
_on_call_bridges_done: bool = PrivateAttr(default=False)
|
|
30
|
+
|
|
31
|
+
async def on_activate(self) -> None:
|
|
32
|
+
await super().on_activate()
|
|
33
|
+
_register_on_call_bridges(self)
|
|
34
|
+
|
|
35
|
+
async def on_inactive(self) -> None:
|
|
36
|
+
self._on_call_bridges_done = False
|
|
37
|
+
await super().on_inactive()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _register_on_call_bridges(ev: OnCallEvent) -> None:
|
|
41
|
+
"""在总线上按本事件的调用规格登记,在匹配到调用后调度 fire。"""
|
|
42
|
+
|
|
43
|
+
if ev._on_call_bridges_done:
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
from observer.context import ObserverContext
|
|
47
|
+
|
|
48
|
+
for spec in ev.on_calls:
|
|
49
|
+
if not spec.method_name:
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
def on_observed_call(
|
|
53
|
+
obs_ctx: ObserverContext, _spec: CallInfo = spec
|
|
54
|
+
) -> None:
|
|
55
|
+
if obs_ctx.phase != "after":
|
|
56
|
+
return
|
|
57
|
+
if _spec.instance_type and obs_ctx.cls_name != _spec.instance_type:
|
|
58
|
+
return
|
|
59
|
+
inst = obs_ctx.instance
|
|
60
|
+
if _spec.instance_name:
|
|
61
|
+
if not isinstance(inst, BaseAutomation):
|
|
62
|
+
return
|
|
63
|
+
if inst.instance_name != _spec.instance_name:
|
|
64
|
+
return
|
|
65
|
+
else:
|
|
66
|
+
if inst is None:
|
|
67
|
+
return
|
|
68
|
+
if not isinstance(inst, BaseAutomation) or inst._ctx is not ev._ctx:
|
|
69
|
+
return
|
|
70
|
+
loop = ev._ctx.main_loop
|
|
71
|
+
if loop is None:
|
|
72
|
+
logger.warning(
|
|
73
|
+
"事件 %s:主循环未就绪,无法在观测回调路径中向主循环调度 fire",
|
|
74
|
+
ev.instance_name,
|
|
75
|
+
)
|
|
76
|
+
return
|
|
77
|
+
fut: ConcurrentFuture[Any] = asyncio.run_coroutine_threadsafe(
|
|
78
|
+
ev.fire(dict(obs_ctx.kwargs)), loop
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def _on_fire_done(done: ConcurrentFuture[Any]) -> None:
|
|
82
|
+
exc = done.exception()
|
|
83
|
+
if exc is not None:
|
|
84
|
+
logger.error(
|
|
85
|
+
"由观测触发的 fire 失败:%s",
|
|
86
|
+
exc,
|
|
87
|
+
exc_info=exc,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
fut.add_done_callback(_on_fire_done)
|
|
91
|
+
|
|
92
|
+
filters: dict[str, Any] = {
|
|
93
|
+
"phase": "after",
|
|
94
|
+
"method_name": spec.method_name,
|
|
95
|
+
}
|
|
96
|
+
if spec.instance_type:
|
|
97
|
+
filters["cls_name"] = spec.instance_type
|
|
98
|
+
observer_bus.subscribe(on_observed_call, **filters)
|
|
99
|
+
ev._ctx.register_observer_bridge_handler(on_observed_call)
|
|
100
|
+
|
|
101
|
+
ev._on_call_bridges_done = True
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
OnCallEvent.registered_kind = "on_call"
|
|
105
|
+
event_registry.register("on_call", OnCallEvent)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from .base import BaseAutomation, registered_kind_for
|
|
2
|
+
from .action import Action
|
|
3
|
+
from .entity import Entity
|
|
4
|
+
from .trigger import Trigger
|
|
5
|
+
from .event import Event
|
|
6
|
+
from .registry_catalog import (
|
|
7
|
+
ACTION_NAMESPACE,
|
|
8
|
+
ENTITY_NAMESPACE,
|
|
9
|
+
EVENT_NAMESPACE,
|
|
10
|
+
TRIGGER_NAMESPACE,
|
|
11
|
+
action_registry,
|
|
12
|
+
catalog_registry,
|
|
13
|
+
entity_registry,
|
|
14
|
+
event_registry,
|
|
15
|
+
instantiate_registered,
|
|
16
|
+
trigger_registry,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"BaseAutomation",
|
|
21
|
+
"Action",
|
|
22
|
+
"Entity",
|
|
23
|
+
"Trigger",
|
|
24
|
+
"Event",
|
|
25
|
+
"catalog_registry",
|
|
26
|
+
"action_registry",
|
|
27
|
+
"entity_registry",
|
|
28
|
+
"event_registry",
|
|
29
|
+
"trigger_registry",
|
|
30
|
+
"instantiate_registered",
|
|
31
|
+
"ENTITY_NAMESPACE",
|
|
32
|
+
"EVENT_NAMESPACE",
|
|
33
|
+
"TRIGGER_NAMESPACE",
|
|
34
|
+
"ACTION_NAMESPACE",
|
|
35
|
+
"registered_kind_for",
|
|
36
|
+
]
|