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.
Files changed (83) hide show
  1. {python_library_automation-0.1.16 → python_library_automation-0.3.0}/.gitignore +3 -1
  2. python_library_automation-0.3.0/PKG-INFO +8 -0
  3. python_library_automation-0.3.0/automation/__init__.py +58 -0
  4. python_library_automation-0.3.0/automation/assistant.py +113 -0
  5. {python_library_automation-0.1.16 → python_library_automation-0.3.0}/automation/builtins/__init__.py +1 -1
  6. python_library_automation-0.3.0/automation/builtins/action/__init__.py +11 -0
  7. python_library_automation-0.3.0/automation/builtins/action/call_method.py +50 -0
  8. python_library_automation-0.3.0/automation/builtins/action/delay.py +25 -0
  9. python_library_automation-0.3.0/automation/builtins/action/log.py +30 -0
  10. python_library_automation-0.3.0/automation/builtins/action/set_attribute.py +42 -0
  11. python_library_automation-0.3.0/automation/builtins/event/attribute_change.py +119 -0
  12. python_library_automation-0.3.0/automation/builtins/event/on_call.py +105 -0
  13. python_library_automation-0.3.0/automation/core/__init__.py +36 -0
  14. python_library_automation-0.3.0/automation/core/action.py +143 -0
  15. python_library_automation-0.3.0/automation/core/base.py +130 -0
  16. python_library_automation-0.3.0/automation/core/entity.py +271 -0
  17. python_library_automation-0.3.0/automation/core/event.py +96 -0
  18. python_library_automation-0.1.16/automation/core/entity.py → python_library_automation-0.3.0/automation/core/info.py +58 -66
  19. python_library_automation-0.3.0/automation/core/registry_catalog.py +87 -0
  20. python_library_automation-0.3.0/automation/core/renderer.py +39 -0
  21. python_library_automation-0.3.0/automation/core/trigger.py +334 -0
  22. {python_library_automation-0.1.16 → python_library_automation-0.3.0}/automation/errors.py +7 -2
  23. python_library_automation-0.3.0/automation/listener/__init__.py +41 -0
  24. python_library_automation-0.3.0/automation/listener/base.py +131 -0
  25. python_library_automation-0.3.0/automation/listener/console.py +101 -0
  26. python_library_automation-0.3.0/automation/listener/events.py +147 -0
  27. python_library_automation-0.3.0/automation/runtime/__init__.py +30 -0
  28. python_library_automation-0.3.0/automation/runtime/bootstrap.py +227 -0
  29. python_library_automation-0.3.0/automation/runtime/context.py +168 -0
  30. python_library_automation-0.3.0/automation/runtime/global_state.py +62 -0
  31. python_library_automation-0.3.0/automation/runtime/main_flow.py +5 -0
  32. python_library_automation-0.3.0/automation/runtime/relaunch.py +57 -0
  33. python_library_automation-0.3.0/debug.bat +14 -0
  34. {python_library_automation-0.1.16 → python_library_automation-0.3.0}/pyproject.toml +4 -4
  35. python_library_automation-0.1.16/PKG-INFO +0 -8
  36. python_library_automation-0.1.16/automation/__init__.py +0 -25
  37. python_library_automation-0.1.16/automation/assistant.py +0 -141
  38. python_library_automation-0.1.16/automation/builtins/action/call_entity_method.py +0 -35
  39. python_library_automation-0.1.16/automation/builtins/action/delay.py +0 -18
  40. python_library_automation-0.1.16/automation/builtins/action/log.py +0 -20
  41. python_library_automation-0.1.16/automation/builtins/action/set_attribute.py +0 -27
  42. python_library_automation-0.1.16/automation/builtins/entity/__init__.py +0 -0
  43. python_library_automation-0.1.16/automation/builtins/entity/time.py +0 -29
  44. python_library_automation-0.1.16/automation/builtins/entity/variable.py +0 -81
  45. python_library_automation-0.1.16/automation/builtins/event/__init__.py +0 -0
  46. python_library_automation-0.1.16/automation/builtins/event/_scheduled.py +0 -34
  47. python_library_automation-0.1.16/automation/builtins/event/at.py +0 -27
  48. python_library_automation-0.1.16/automation/builtins/event/callback.py +0 -65
  49. python_library_automation-0.1.16/automation/builtins/event/every.py +0 -32
  50. python_library_automation-0.1.16/automation/builtins/event/state_changed.py +0 -58
  51. python_library_automation-0.1.16/automation/core/__init__.py +0 -13
  52. python_library_automation-0.1.16/automation/core/action.py +0 -22
  53. python_library_automation-0.1.16/automation/core/base.py +0 -59
  54. python_library_automation-0.1.16/automation/core/composite_action.py +0 -47
  55. python_library_automation-0.1.16/automation/core/event.py +0 -87
  56. python_library_automation-0.1.16/automation/core/event_context.py +0 -10
  57. python_library_automation-0.1.16/automation/core/trigger.py +0 -151
  58. python_library_automation-0.1.16/automation/executor.py +0 -46
  59. python_library_automation-0.1.16/automation/hub.py +0 -52
  60. python_library_automation-0.1.16/automation/listeners/__init__.py +0 -17
  61. python_library_automation-0.1.16/automation/listeners/base.py +0 -23
  62. python_library_automation-0.1.16/automation/listeners/console.py +0 -82
  63. python_library_automation-0.1.16/automation/listeners/instance_schema.py +0 -26
  64. python_library_automation-0.1.16/automation/listeners/record.py +0 -29
  65. python_library_automation-0.1.16/automation/listeners/trace.py +0 -70
  66. python_library_automation-0.1.16/automation/listeners/type_schema.py +0 -26
  67. python_library_automation-0.1.16/automation/loader.py +0 -131
  68. python_library_automation-0.1.16/automation/renderer.py +0 -311
  69. python_library_automation-0.1.16/automation/schema.py +0 -142
  70. python_library_automation-0.1.16/automation/updater.py +0 -84
  71. python_library_automation-0.1.16/example-simple.bat +0 -10
  72. python_library_automation-0.1.16/examples/simple/__main__.py +0 -27
  73. python_library_automation-0.1.16/tests/support.py +0 -20
  74. python_library_automation-0.1.16/tests/test_assistant.py +0 -31
  75. python_library_automation-0.1.16/tests/test_builder.py +0 -43
  76. python_library_automation-0.1.16/tests/test_expression_condition.py +0 -103
  77. python_library_automation-0.1.16/tests/test_expression_parser.py +0 -38
  78. python_library_automation-0.1.16/tests/test_hub.py +0 -20
  79. python_library_automation-0.1.16/tests/test_schema.py +0 -179
  80. python_library_automation-0.1.16/tests/test_trigger_flow.py +0 -92
  81. {python_library_automation-0.1.16/automation/builtins/action → python_library_automation-0.3.0/automation/builtins/event}/__init__.py +0 -0
  82. {python_library_automation-0.1.16 → python_library_automation-0.3.0}/test.bat +0 -0
  83. {python_library_automation-0.1.16 → python_library_automation-0.3.0}/update.bat +0 -0
@@ -8,4 +8,6 @@ build/
8
8
  .env
9
9
  .pytest_cache/
10
10
  config.yaml
11
- logs/
11
+ logs/
12
+ .cursor/
13
+ uv.lock
@@ -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
+ ]
@@ -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
+ ]