fastpilot 0.1.0__py3-none-any.whl
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.
- fastpilot/__init__.py +171 -0
- fastpilot/contrib/__init__.py +30 -0
- fastpilot/contrib/echo.py +61 -0
- fastpilot/contrib/robot.py +168 -0
- fastpilot/core/__init__.py +7 -0
- fastpilot/core/children.py +105 -0
- fastpilot/core/factory.py +34 -0
- fastpilot/core/handle.py +59 -0
- fastpilot/core/logical_time.py +142 -0
- fastpilot/core/queue.py +184 -0
- fastpilot/core/realtime.py +61 -0
- fastpilot/core/runtime.py +781 -0
- fastpilot/core/station.py +141 -0
- fastpilot/driver.py +175 -0
- fastpilot/ipython/__init__.py +9 -0
- fastpilot/ipython/display.py +54 -0
- fastpilot/observation/__init__.py +38 -0
- fastpilot/observation/bus.py +185 -0
- fastpilot/observation/runtime_history.py +98 -0
- fastpilot/observation/store.py +245 -0
- fastpilot/observation/updates.py +111 -0
- fastpilot/protocol/__init__.py +147 -0
- fastpilot/protocol/catalog.py +465 -0
- fastpilot/protocol/commands.py +154 -0
- fastpilot/protocol/contracts.py +69 -0
- fastpilot/protocol/declarations.py +161 -0
- fastpilot/protocol/drafts.py +70 -0
- fastpilot/protocol/effects.py +218 -0
- fastpilot/protocol/errors.py +13 -0
- fastpilot/protocol/events.py +117 -0
- fastpilot/protocol/hosting.py +20 -0
- fastpilot/protocol/queue.py +77 -0
- fastpilot/protocol/receipts.py +50 -0
- fastpilot/protocol/schema.py +144 -0
- fastpilot/protocol/system_facts.py +228 -0
- fastpilot/protocol/triggers.py +227 -0
- fastpilot/protocol/values.py +111 -0
- fastpilot/remote/__init__.py +7 -0
- fastpilot/remote/connect.py +86 -0
- fastpilot/remote/errors.py +21 -0
- fastpilot/remote/handle.py +47 -0
- fastpilot/remote/runtime.py +530 -0
- fastpilot/service/__init__.py +23 -0
- fastpilot/service/__main__.py +32 -0
- fastpilot/service/app.py +486 -0
- fastpilot/service/browser.py +191 -0
- fastpilot/service/hosting.py +232 -0
- fastpilot/service/ledger.py +43 -0
- fastpilot/service/mount.py +53 -0
- fastpilot/service/streaming.py +53 -0
- fastpilot/state/__init__.py +32 -0
- fastpilot/state/chart.py +46 -0
- fastpilot/state/handlers.py +136 -0
- fastpilot/state/machine.py +169 -0
- fastpilot/state/participant.py +227 -0
- fastpilot/time/__init__.py +27 -0
- fastpilot/time/calendar.py +48 -0
- fastpilot/time/clock.py +31 -0
- fastpilot/time/rules.py +63 -0
- fastpilot/time/validation.py +25 -0
- fastpilot/wire/__init__.py +39 -0
- fastpilot/wire/commands.py +62 -0
- fastpilot/wire/observations.py +345 -0
- fastpilot/wire/protocol_values.py +307 -0
- fastpilot/wire/values.py +102 -0
- fastpilot/wire/version.py +5 -0
- fastpilot-0.1.0.dist-info/METADATA +384 -0
- fastpilot-0.1.0.dist-info/RECORD +70 -0
- fastpilot-0.1.0.dist-info/WHEEL +4 -0
- fastpilot-0.1.0.dist-info/licenses/LICENSE +203 -0
fastpilot/__init__.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""FastPilot:Python 业务对象的控制、观察与远程访问协议。"""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from fastpilot.core import Runtime, RuntimeHandle, make_runtime
|
|
6
|
+
from fastpilot.driver import CommandSurface, Driver
|
|
7
|
+
from fastpilot.observation import (
|
|
8
|
+
HistoryStore,
|
|
9
|
+
HistoryStoreFactory,
|
|
10
|
+
MemoryHistoryStore,
|
|
11
|
+
Predicate,
|
|
12
|
+
PredicateLike,
|
|
13
|
+
RuntimeHistoryStores,
|
|
14
|
+
SQLiteSpoolHistoryStore,
|
|
15
|
+
Subscription,
|
|
16
|
+
temporary_sqlite_history,
|
|
17
|
+
)
|
|
18
|
+
from fastpilot.protocol import (
|
|
19
|
+
Advance,
|
|
20
|
+
AllOf,
|
|
21
|
+
AnyOf,
|
|
22
|
+
AtTime,
|
|
23
|
+
CancelEntry,
|
|
24
|
+
CapabilityError,
|
|
25
|
+
Command,
|
|
26
|
+
CommandEffect,
|
|
27
|
+
CommandOutcome,
|
|
28
|
+
CommandQueued,
|
|
29
|
+
CommandReceipt,
|
|
30
|
+
Commit,
|
|
31
|
+
DomainCommandHandled,
|
|
32
|
+
DraftConflictError,
|
|
33
|
+
Enqueue,
|
|
34
|
+
Event,
|
|
35
|
+
Fact,
|
|
36
|
+
FastPilotError,
|
|
37
|
+
HostedRuntimeView,
|
|
38
|
+
Immediately,
|
|
39
|
+
MoveEntry,
|
|
40
|
+
OnFact,
|
|
41
|
+
OnStateEntered,
|
|
42
|
+
Pause,
|
|
43
|
+
Paused,
|
|
44
|
+
QueueEntryStatus,
|
|
45
|
+
QueueEntryView,
|
|
46
|
+
QueueSnapshot,
|
|
47
|
+
Reject,
|
|
48
|
+
Resume,
|
|
49
|
+
Resumed,
|
|
50
|
+
RuntimeUpdate,
|
|
51
|
+
SetSpeed,
|
|
52
|
+
Snapshot,
|
|
53
|
+
Start,
|
|
54
|
+
Started,
|
|
55
|
+
StateEntered,
|
|
56
|
+
Stop,
|
|
57
|
+
Stopped,
|
|
58
|
+
TransitionDraft,
|
|
59
|
+
TransitionView,
|
|
60
|
+
Trigger,
|
|
61
|
+
WhenState,
|
|
62
|
+
command,
|
|
63
|
+
fact,
|
|
64
|
+
framework_protocol,
|
|
65
|
+
)
|
|
66
|
+
from fastpilot.state import (
|
|
67
|
+
DataParticipant,
|
|
68
|
+
ParticipantContext,
|
|
69
|
+
RuntimeParticipant,
|
|
70
|
+
StateChart,
|
|
71
|
+
StatefulParticipant,
|
|
72
|
+
TickParticipant,
|
|
73
|
+
emits,
|
|
74
|
+
handles,
|
|
75
|
+
participant_protocol,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if TYPE_CHECKING:
|
|
79
|
+
from fastpilot.remote import RemoteRuntime
|
|
80
|
+
|
|
81
|
+
__version__ = "0.1.0"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def connect(
|
|
85
|
+
base_url: str,
|
|
86
|
+
*,
|
|
87
|
+
connect_timeout: float = 3.0,
|
|
88
|
+
command_timeout: float = 30.0,
|
|
89
|
+
) -> "RemoteRuntime":
|
|
90
|
+
"""连接独立 FastPilot 服务,并延迟加载远程可选依赖。"""
|
|
91
|
+
from fastpilot.remote import connect as remote_connect
|
|
92
|
+
|
|
93
|
+
return remote_connect(
|
|
94
|
+
base_url,
|
|
95
|
+
connect_timeout=connect_timeout,
|
|
96
|
+
command_timeout=command_timeout,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
__all__ = [
|
|
101
|
+
"Advance",
|
|
102
|
+
"AllOf",
|
|
103
|
+
"AnyOf",
|
|
104
|
+
"AtTime",
|
|
105
|
+
"CancelEntry",
|
|
106
|
+
"CapabilityError",
|
|
107
|
+
"Command",
|
|
108
|
+
"CommandQueued",
|
|
109
|
+
"CommandOutcome",
|
|
110
|
+
"CommandEffect",
|
|
111
|
+
"CommandReceipt",
|
|
112
|
+
"CommandSurface",
|
|
113
|
+
"Commit",
|
|
114
|
+
"DomainCommandHandled",
|
|
115
|
+
"DataParticipant",
|
|
116
|
+
"DraftConflictError",
|
|
117
|
+
"Driver",
|
|
118
|
+
"Enqueue",
|
|
119
|
+
"Event",
|
|
120
|
+
"Fact",
|
|
121
|
+
"FastPilotError",
|
|
122
|
+
"HistoryStore",
|
|
123
|
+
"HistoryStoreFactory",
|
|
124
|
+
"HostedRuntimeView",
|
|
125
|
+
"Immediately",
|
|
126
|
+
"MemoryHistoryStore",
|
|
127
|
+
"MoveEntry",
|
|
128
|
+
"OnFact",
|
|
129
|
+
"OnStateEntered",
|
|
130
|
+
"ParticipantContext",
|
|
131
|
+
"Pause",
|
|
132
|
+
"Paused",
|
|
133
|
+
"Predicate",
|
|
134
|
+
"PredicateLike",
|
|
135
|
+
"QueueEntryStatus",
|
|
136
|
+
"QueueEntryView",
|
|
137
|
+
"QueueSnapshot",
|
|
138
|
+
"Reject",
|
|
139
|
+
"Resume",
|
|
140
|
+
"Resumed",
|
|
141
|
+
"Runtime",
|
|
142
|
+
"RuntimeHandle",
|
|
143
|
+
"RuntimeHistoryStores",
|
|
144
|
+
"RuntimeParticipant",
|
|
145
|
+
"RuntimeUpdate",
|
|
146
|
+
"SetSpeed",
|
|
147
|
+
"Snapshot",
|
|
148
|
+
"SQLiteSpoolHistoryStore",
|
|
149
|
+
"Start",
|
|
150
|
+
"Started",
|
|
151
|
+
"StateEntered",
|
|
152
|
+
"StatefulParticipant",
|
|
153
|
+
"StateChart",
|
|
154
|
+
"Stop",
|
|
155
|
+
"Stopped",
|
|
156
|
+
"Subscription",
|
|
157
|
+
"TickParticipant",
|
|
158
|
+
"TransitionDraft",
|
|
159
|
+
"TransitionView",
|
|
160
|
+
"Trigger",
|
|
161
|
+
"WhenState",
|
|
162
|
+
"command",
|
|
163
|
+
"emits",
|
|
164
|
+
"fact",
|
|
165
|
+
"framework_protocol",
|
|
166
|
+
"handles",
|
|
167
|
+
"participant_protocol",
|
|
168
|
+
"temporary_sqlite_history",
|
|
169
|
+
"connect",
|
|
170
|
+
"make_runtime",
|
|
171
|
+
]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""用于验证 FastPilot 公共协议的具体参与者。"""
|
|
2
|
+
|
|
3
|
+
from fastpilot.contrib.echo import Echo, Echoed, EchoMessage, make_echo
|
|
4
|
+
from fastpilot.contrib.robot import (
|
|
5
|
+
ROBOT_STATECHART,
|
|
6
|
+
ContinueWalking,
|
|
7
|
+
Obstacle,
|
|
8
|
+
Rest,
|
|
9
|
+
RobotParticipant,
|
|
10
|
+
SetEnergy,
|
|
11
|
+
Wake,
|
|
12
|
+
Walk,
|
|
13
|
+
make_robot,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"Echo",
|
|
18
|
+
"EchoMessage",
|
|
19
|
+
"Echoed",
|
|
20
|
+
"ROBOT_STATECHART",
|
|
21
|
+
"ContinueWalking",
|
|
22
|
+
"Obstacle",
|
|
23
|
+
"Rest",
|
|
24
|
+
"RobotParticipant",
|
|
25
|
+
"SetEnergy",
|
|
26
|
+
"Wake",
|
|
27
|
+
"Walk",
|
|
28
|
+
"make_echo",
|
|
29
|
+
"make_robot",
|
|
30
|
+
]
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Echo:用最小类型化 Command/Fact 展示事件驱动业务。"""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
|
|
6
|
+
from fastpilot.core import Runtime, make_runtime
|
|
7
|
+
from fastpilot.protocol import Command, Fact, command, fact
|
|
8
|
+
from fastpilot.protocol.values import freeze_mapping
|
|
9
|
+
from fastpilot.state import ParticipantContext, emits, handles
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@command("fastpilot.contrib.echo.message")
|
|
13
|
+
@dataclass(frozen=True, slots=True)
|
|
14
|
+
class EchoMessage(Command):
|
|
15
|
+
"""一条由 Echo 记录并原样发布的领域消息。"""
|
|
16
|
+
|
|
17
|
+
name: str
|
|
18
|
+
payload: Mapping[str, object] = field(default_factory=dict)
|
|
19
|
+
|
|
20
|
+
def __post_init__(self) -> None:
|
|
21
|
+
object.__setattr__(self, "payload", freeze_mapping(self.payload))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@fact("fastpilot.contrib.echo.echoed")
|
|
25
|
+
@dataclass(frozen=True, slots=True)
|
|
26
|
+
class Echoed(Fact):
|
|
27
|
+
"""Echo 已经接收并保存一条消息。"""
|
|
28
|
+
|
|
29
|
+
name: str
|
|
30
|
+
payload: Mapping[str, object] = field(default_factory=dict)
|
|
31
|
+
|
|
32
|
+
def __post_init__(self) -> None:
|
|
33
|
+
object.__setattr__(self, "payload", freeze_mapping(self.payload))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Echo:
|
|
37
|
+
"""把最近一次输入定义为 Echo 的领域状态,并发布对应领域事实。
|
|
38
|
+
|
|
39
|
+
``last_message`` 与 ``last_payload`` 由 Echo 在回调中主动写入。这个选择
|
|
40
|
+
属于 Echo 的领域模型,使 Snapshot 可以回答“最近回显了什么”。
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
@handles
|
|
44
|
+
@emits(Echoed)
|
|
45
|
+
def echo(
|
|
46
|
+
self,
|
|
47
|
+
context: ParticipantContext,
|
|
48
|
+
command: EchoMessage,
|
|
49
|
+
) -> None:
|
|
50
|
+
context.write_data("last_message", command.name)
|
|
51
|
+
context.write_data("last_payload", dict(command.payload))
|
|
52
|
+
context.emit(Echoed(command.name, command.payload))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def make_echo() -> Runtime:
|
|
56
|
+
"""创建一个可在 IPython 中直接驱动的 Echo Runtime。"""
|
|
57
|
+
|
|
58
|
+
return make_runtime(Echo())
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
__all__ = ["Echo", "EchoMessage", "Echoed", "make_echo"]
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""散步机器人:证明状态图、逻辑时间和草稿站的厚业务参与者。"""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
from fastpilot.core import Runtime, make_runtime
|
|
7
|
+
from fastpilot.protocol import Command, TransitionDraft, command
|
|
8
|
+
from fastpilot.state import ParticipantContext, StateChart, handles
|
|
9
|
+
|
|
10
|
+
ROBOT_STATECHART = StateChart.create(
|
|
11
|
+
initial="IDLE",
|
|
12
|
+
transitions={
|
|
13
|
+
"IDLE": frozenset({"WALKING"}),
|
|
14
|
+
"WALKING": frozenset({"THINKING", "RESTING", "CHARGING"}),
|
|
15
|
+
"THINKING": frozenset({"WALKING", "RESTING"}),
|
|
16
|
+
"RESTING": frozenset({"WALKING"}),
|
|
17
|
+
"CHARGING": frozenset({"WALKING"}),
|
|
18
|
+
},
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@command("fastpilot.contrib.robot.walk")
|
|
23
|
+
@dataclass(frozen=True, slots=True)
|
|
24
|
+
class Walk(Command):
|
|
25
|
+
"""请求空闲 Robot 建立进入 WALKING 的转换草稿。"""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@command("fastpilot.contrib.robot.obstacle")
|
|
29
|
+
@dataclass(frozen=True, slots=True)
|
|
30
|
+
class Obstacle(Command):
|
|
31
|
+
"""报告行走路径出现障碍。"""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@command("fastpilot.contrib.robot.rest")
|
|
35
|
+
@dataclass(frozen=True, slots=True)
|
|
36
|
+
class Rest(Command):
|
|
37
|
+
"""请求思考中的 Robot 进入休息状态。"""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@command("fastpilot.contrib.robot.continue_walking")
|
|
41
|
+
@dataclass(frozen=True, slots=True)
|
|
42
|
+
class ContinueWalking(Command):
|
|
43
|
+
"""请求思考中的 Robot 继续行走。"""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@command("fastpilot.contrib.robot.wake")
|
|
47
|
+
@dataclass(frozen=True, slots=True)
|
|
48
|
+
class Wake(Command):
|
|
49
|
+
"""请求休息中的 Robot 重新行走。"""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@command("fastpilot.contrib.robot.set_energy")
|
|
53
|
+
@dataclass(frozen=True, slots=True)
|
|
54
|
+
class SetEnergy(Command):
|
|
55
|
+
"""把 Robot 的领域能量设置为一个明确数值。"""
|
|
56
|
+
|
|
57
|
+
value: float
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True, slots=True)
|
|
61
|
+
class RobotParticipant:
|
|
62
|
+
"""以位置、能量和状态边展示完整 Runtime 能力。"""
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def statechart(self) -> StateChart:
|
|
66
|
+
return ROBOT_STATECHART
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def initial_data(self) -> Mapping[str, object]:
|
|
70
|
+
return {"position": 0.0, "energy": 100.0, "mood": "happy"}
|
|
71
|
+
|
|
72
|
+
def on_tick(
|
|
73
|
+
self,
|
|
74
|
+
context: ParticipantContext,
|
|
75
|
+
delta: float,
|
|
76
|
+
) -> TransitionDraft | None:
|
|
77
|
+
if context.state == "WALKING":
|
|
78
|
+
position = _as_float(context.read_data("position", 0.0)) + delta
|
|
79
|
+
energy = _as_float(context.read_data("energy", 100.0)) - delta
|
|
80
|
+
context.write_data("position", position)
|
|
81
|
+
context.write_data("energy", energy)
|
|
82
|
+
if energy <= 20:
|
|
83
|
+
return context.propose("CHARGING", "energy_low", {"energy": energy})
|
|
84
|
+
if context.state == "RESTING" and delta > 0:
|
|
85
|
+
energy = min(100.0, _as_float(context.read_data("energy", 100.0)) + delta * 2)
|
|
86
|
+
context.write_data("energy", energy)
|
|
87
|
+
if context.state == "CHARGING" and delta > 0:
|
|
88
|
+
energy = min(100.0, _as_float(context.read_data("energy", 0.0)) + delta * 5)
|
|
89
|
+
context.write_data("energy", energy)
|
|
90
|
+
if energy >= 100:
|
|
91
|
+
return context.propose("WALKING", "charged")
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
@handles
|
|
95
|
+
def set_energy(self, context: ParticipantContext, command: SetEnergy) -> None:
|
|
96
|
+
context.write_data("energy", float(command.value))
|
|
97
|
+
|
|
98
|
+
@handles
|
|
99
|
+
def walk(self, context: ParticipantContext, command: Walk) -> TransitionDraft | None:
|
|
100
|
+
del command
|
|
101
|
+
if context.state == "IDLE":
|
|
102
|
+
return context.propose("WALKING", "walk_command")
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
@handles
|
|
106
|
+
def obstacle(
|
|
107
|
+
self,
|
|
108
|
+
context: ParticipantContext,
|
|
109
|
+
command: Obstacle,
|
|
110
|
+
) -> TransitionDraft | None:
|
|
111
|
+
del command
|
|
112
|
+
if context.state == "WALKING":
|
|
113
|
+
return context.propose(
|
|
114
|
+
"THINKING",
|
|
115
|
+
"obstacle",
|
|
116
|
+
{"position": context.read_data("position", 0.0)},
|
|
117
|
+
)
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
@handles
|
|
121
|
+
def rest(self, context: ParticipantContext, command: Rest) -> TransitionDraft | None:
|
|
122
|
+
del command
|
|
123
|
+
if context.state == "THINKING":
|
|
124
|
+
return context.propose("RESTING", "driver_decision")
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
@handles
|
|
128
|
+
def continue_walking(
|
|
129
|
+
self,
|
|
130
|
+
context: ParticipantContext,
|
|
131
|
+
command: ContinueWalking,
|
|
132
|
+
) -> TransitionDraft | None:
|
|
133
|
+
del command
|
|
134
|
+
if context.state == "THINKING":
|
|
135
|
+
return context.propose("WALKING", "driver_decision")
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
@handles
|
|
139
|
+
def wake(self, context: ParticipantContext, command: Wake) -> TransitionDraft | None:
|
|
140
|
+
del command
|
|
141
|
+
if context.state == "RESTING":
|
|
142
|
+
return context.propose("WALKING", "wake_command")
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def make_robot() -> Runtime:
|
|
147
|
+
"""创建散步机器人 Runtime。"""
|
|
148
|
+
return make_runtime(RobotParticipant())
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _as_float(value: object) -> float:
|
|
152
|
+
if not isinstance(value, (int, float)):
|
|
153
|
+
return 0.0
|
|
154
|
+
result = float(value)
|
|
155
|
+
return result if result == result and abs(result) != float("inf") else 0.0
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
__all__ = [
|
|
159
|
+
"ROBOT_STATECHART",
|
|
160
|
+
"ContinueWalking",
|
|
161
|
+
"Obstacle",
|
|
162
|
+
"Rest",
|
|
163
|
+
"RobotParticipant",
|
|
164
|
+
"SetEnergy",
|
|
165
|
+
"Wake",
|
|
166
|
+
"Walk",
|
|
167
|
+
"make_robot",
|
|
168
|
+
]
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""父子 Runtime 的托管树与稳定路径解析。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from fastpilot.core.handle import RuntimeHandle
|
|
9
|
+
from fastpilot.protocol.contracts import Fact
|
|
10
|
+
from fastpilot.protocol.events import Event
|
|
11
|
+
from fastpilot.protocol.hosting import HostedRuntimeView
|
|
12
|
+
from fastpilot.protocol.system_facts import ChildAttached, ChildDetached
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from fastpilot.core.runtime import Runtime
|
|
16
|
+
|
|
17
|
+
type EmitEvent = Callable[[Fact], Event]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ChildRegistry:
|
|
21
|
+
"""维护一个 Runtime 直接托管的子 Runtime 与递归数字路径。"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, owner: Runtime, emit: EmitEvent) -> None:
|
|
24
|
+
self._owner = owner
|
|
25
|
+
self._emit = emit
|
|
26
|
+
self._children: dict[int, RuntimeHandle] = {}
|
|
27
|
+
self._next_id = 1
|
|
28
|
+
|
|
29
|
+
def attach(self, child: Runtime, name: str | None = None) -> RuntimeHandle:
|
|
30
|
+
"""登记子 Runtime,并验证托管树保持单父级、非自指和无环。"""
|
|
31
|
+
if self._owner.status == "stopped":
|
|
32
|
+
raise RuntimeError("已停止的 Runtime 不能再托管子 Runtime")
|
|
33
|
+
if child is self._owner:
|
|
34
|
+
raise ValueError("运行时不能托管自己")
|
|
35
|
+
if child._parent is not None:
|
|
36
|
+
raise RuntimeError("子 Runtime 已经被其他父 Runtime 托管")
|
|
37
|
+
if self._contains_ancestor(child):
|
|
38
|
+
raise ValueError("托管关系会形成循环")
|
|
39
|
+
child_id = self._next_id
|
|
40
|
+
self._next_id += 1
|
|
41
|
+
handle = RuntimeHandle(child_id, name or f"child-{child_id}", child)
|
|
42
|
+
self._children[child_id] = handle
|
|
43
|
+
child._parent = self._owner
|
|
44
|
+
self._emit(ChildAttached(child_id, handle.name))
|
|
45
|
+
return handle
|
|
46
|
+
|
|
47
|
+
def handles(self) -> tuple[RuntimeHandle, ...]:
|
|
48
|
+
"""读取直接子 Runtime 的稳定句柄。"""
|
|
49
|
+
return tuple(self._children.values())
|
|
50
|
+
|
|
51
|
+
def views(self) -> tuple[HostedRuntimeView, ...]:
|
|
52
|
+
"""读取可嵌入父快照的子 Runtime 投影。"""
|
|
53
|
+
return tuple(handle.view() for handle in self._children.values())
|
|
54
|
+
|
|
55
|
+
def resolve(self, path: str) -> Runtime:
|
|
56
|
+
"""沿 ``1/2/...`` 数字路径解析递归子 Runtime。"""
|
|
57
|
+
current = self._owner
|
|
58
|
+
for segment in filter(None, path.split("/")):
|
|
59
|
+
try:
|
|
60
|
+
child_id = int(segment)
|
|
61
|
+
except ValueError as exc:
|
|
62
|
+
raise KeyError(f"子 Runtime 编号无效:{segment}") from exc
|
|
63
|
+
with current._lock:
|
|
64
|
+
handle = current._children.get(child_id)
|
|
65
|
+
current = handle._runtime
|
|
66
|
+
return current
|
|
67
|
+
|
|
68
|
+
def get(self, child_id: int) -> RuntimeHandle:
|
|
69
|
+
"""按稳定编号读取一个直接子 Runtime 句柄。"""
|
|
70
|
+
handle = self._children.get(child_id)
|
|
71
|
+
if handle is None:
|
|
72
|
+
raise KeyError(f"子 Runtime 不存在:{child_id}")
|
|
73
|
+
return handle
|
|
74
|
+
|
|
75
|
+
def detach(self, child_id: int, *, stop: bool = False) -> HostedRuntimeView:
|
|
76
|
+
"""移除子 Runtime,并返回解除关系前的投影。"""
|
|
77
|
+
handle = self._children.get(child_id)
|
|
78
|
+
if handle is None:
|
|
79
|
+
raise KeyError(f"不存在的子运行时:{child_id}")
|
|
80
|
+
child_runtime = handle.open()
|
|
81
|
+
if stop and child_runtime.snapshot().status != "stopped":
|
|
82
|
+
child_runtime.stop()
|
|
83
|
+
child = handle._runtime
|
|
84
|
+
child._parent = None
|
|
85
|
+
del self._children[child_id]
|
|
86
|
+
self._emit(ChildDetached(child_id, handle.name))
|
|
87
|
+
return handle.view()
|
|
88
|
+
|
|
89
|
+
def stop_all(self) -> None:
|
|
90
|
+
"""沿每个子 Runtime 的控制面传播父级终止。"""
|
|
91
|
+
for handle in tuple(self._children.values()):
|
|
92
|
+
child = handle.open()
|
|
93
|
+
if child.snapshot().status != "stopped":
|
|
94
|
+
child.stop()
|
|
95
|
+
|
|
96
|
+
def _contains_ancestor(self, candidate: Runtime) -> bool:
|
|
97
|
+
cursor: Runtime | None = self._owner
|
|
98
|
+
while cursor is not None:
|
|
99
|
+
if cursor is candidate:
|
|
100
|
+
return True
|
|
101
|
+
cursor = cursor._parent
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
__all__ = ["ChildRegistry"]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""根据参与者实际能力创建 FastPilot Runtime。"""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
|
|
5
|
+
from fastpilot.core.runtime import Runtime
|
|
6
|
+
from fastpilot.observation.runtime_history import HistoryStoreFactory
|
|
7
|
+
from fastpilot.protocol import framework_protocol
|
|
8
|
+
from fastpilot.state import DataParticipant, StateMachine, participant_protocol
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def make_runtime(
|
|
12
|
+
participant: object,
|
|
13
|
+
*,
|
|
14
|
+
history: HistoryStoreFactory | None = None,
|
|
15
|
+
) -> Runtime:
|
|
16
|
+
"""把一个领域参与者装配成可控制、可观察的 Runtime。
|
|
17
|
+
|
|
18
|
+
工厂根据参与者提供的数据、时间与状态能力组装 Runtime。参与者表达领域
|
|
19
|
+
规则,Runtime 组织命令、时间、队列、事件和草稿边界。
|
|
20
|
+
"""
|
|
21
|
+
initial_data: Mapping[str, object] = (
|
|
22
|
+
participant.initial_data if isinstance(participant, DataParticipant) else {}
|
|
23
|
+
)
|
|
24
|
+
protocol = framework_protocol().merge(participant_protocol(participant))
|
|
25
|
+
history_stores = None if history is None else history(protocol)
|
|
26
|
+
return Runtime(
|
|
27
|
+
StateMachine.from_participant(participant),
|
|
28
|
+
dict(initial_data),
|
|
29
|
+
protocol=protocol,
|
|
30
|
+
history_stores=history_stores,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
__all__ = ["make_runtime"]
|
fastpilot/core/handle.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""父子 Runtime 之间的位置句柄。
|
|
2
|
+
|
|
3
|
+
父 Runtime 把子 Runtime 登记到托管树中,并通过句柄获得稳定编号、名称和
|
|
4
|
+
观察投影。``open()`` 进入子 Runtime 自己的控制面,因此每一层都沿自己的
|
|
5
|
+
时钟、锁、历史和安全边界推进。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
from fastpilot.protocol.hosting import HostedRuntimeView
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from fastpilot.core.runtime import Runtime
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RuntimeHandle:
|
|
19
|
+
"""父 Runtime 持有的子 Runtime 位置句柄。"""
|
|
20
|
+
|
|
21
|
+
__slots__ = ("_id", "_name", "_runtime")
|
|
22
|
+
|
|
23
|
+
def __init__(self, entry_id: int, name: str, runtime: Runtime) -> None:
|
|
24
|
+
self._id = entry_id
|
|
25
|
+
self._name = name
|
|
26
|
+
self._runtime = runtime
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def id(self) -> int:
|
|
30
|
+
"""读取托管树中的稳定子 Runtime 编号。"""
|
|
31
|
+
return self._id
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def name(self) -> str:
|
|
35
|
+
"""读取父 Runtime 用于展示和诊断的子 Runtime 名称。"""
|
|
36
|
+
return self._name
|
|
37
|
+
|
|
38
|
+
def open(self) -> Runtime:
|
|
39
|
+
"""进入子 Runtime 自己的完整控制面。
|
|
40
|
+
|
|
41
|
+
句柄表达父级托管树中的位置与名称;``open()`` 返回孩子自身拥有的
|
|
42
|
+
完整 Runtime。
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
return self._runtime
|
|
46
|
+
|
|
47
|
+
def view(self) -> HostedRuntimeView:
|
|
48
|
+
"""把子 Runtime 快照压缩成可嵌入父快照的只读投影。"""
|
|
49
|
+
snapshot = self._runtime.snapshot()
|
|
50
|
+
return HostedRuntimeView(
|
|
51
|
+
self._id,
|
|
52
|
+
self._name,
|
|
53
|
+
snapshot.status,
|
|
54
|
+
snapshot.time,
|
|
55
|
+
snapshot.state,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def __repr__(self) -> str:
|
|
59
|
+
return f"RuntimeHandle(id={self._id}, name={self._name!r})"
|