rainbow-rb-flow-manager 0.0.9.dev5__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.
File without changes
@@ -0,0 +1,610 @@
1
+ import builtins
2
+ import contextlib
3
+ import queue
4
+ import sys
5
+ import time
6
+ from collections.abc import (
7
+ Callable,
8
+ MutableMapping,
9
+ )
10
+ from multiprocessing import (
11
+ Queue,
12
+ )
13
+ from multiprocessing.synchronize import (
14
+ Event as EventType,
15
+ )
16
+ from typing import Any, Literal, cast
17
+
18
+ from rainbow.rb_sdk.amr import RBAmrSDK
19
+ from rainbow.rb_sdk.base import RBBaseSDK
20
+ from rainbow.rb_sdk.manipulate import RBManipulateSDK
21
+ from rainbow.rb_sdk.program_sdk.program import RBProgramSDK
22
+ from rainbow.rb_utils.parser import t_to_dict
23
+
24
+ from .exception import BreakFolder, StopExecution, SubTaskHaltException
25
+ from .schema import ExecutorState, RB_Flow_Manager_ProgramState
26
+
27
+
28
+ class ExecutionContext:
29
+ """스크립트 실행 컨텍스트"""
30
+
31
+ def __init__(
32
+ self,
33
+ process_id: str,
34
+ state_dict: dict[str, Any],
35
+ result_queue: Queue,
36
+ pause_event: EventType,
37
+ resume_event: EventType,
38
+ stop_event: EventType,
39
+ *,
40
+ min_step_interval: float | None = None,
41
+ step_mode: bool = False,
42
+ shared_state_dicts: dict[str, MutableMapping[str, Any]] | None = None,
43
+ ):
44
+ self.process_id = process_id
45
+ self.state_dict: ExecutorState = cast(ExecutorState, state_dict)
46
+ self.parent_process_id = state_dict.get("parent_process_id")
47
+ self.pause_event = pause_event
48
+ self.resume_event = resume_event
49
+ self.stop_event = stop_event
50
+ self.result_queue = result_queue
51
+ self.variables: dict[str, dict[str, Any]] = {
52
+ "local": {},
53
+ "global": {},
54
+ }
55
+ self._dirty_local_variables: set[str] = set()
56
+ self._dirty_global_variables: set[str] = set()
57
+ self.sdk_functions: dict[str, Callable] = {}
58
+ self._sdk_roots: dict[str, Any] | None = None
59
+ self._arg_scope: list[dict[str, Any]] = []
60
+ self._generation = state_dict.get("generation")
61
+ self.min_step_interval = min_step_interval
62
+ self._folder_depth = 0
63
+ self.step_mode = step_mode
64
+ self.step_num = 1
65
+ self.current_depth = 0
66
+ self._step_ticket = 1 if not step_mode else 0
67
+ self.is_ui_execution = state_dict.get("is_ui_execution", False)
68
+ self.data: dict[str, Any] = {} # 사용자 정의 데이터 저장소
69
+ self.shared_state_dicts = shared_state_dicts
70
+
71
+ # parent_process_id가 있으면 부모 변수 저장소를 owner로 사용
72
+ self.sync_state_variables()
73
+ # 서브프로세스에서 첫 스텝 호출 시점의 lazy init 레이스/지연을 줄이기 위해
74
+ # ExecutionContext 생성 시 SDK 루트를 선초기화한다.
75
+ self._ensure_sdk_roots()
76
+
77
+ def _resolve_variable_owner_state(self) -> ExecutorState:
78
+ if self.parent_process_id is not None and self.shared_state_dicts is not None:
79
+ parent_state = self.shared_state_dicts.get(self.parent_process_id)
80
+ if parent_state is not None:
81
+ return cast(ExecutorState, parent_state)
82
+ return self.state_dict
83
+
84
+ @staticmethod
85
+ def _normalize_state_variables(raw: Any) -> dict[str, dict[str, Any]]:
86
+ if not isinstance(raw, dict):
87
+ return {}
88
+ normalized: dict[str, dict[str, Any]] = {}
89
+ for key, payload in raw.items():
90
+ if not isinstance(payload, dict) or "value" not in payload:
91
+ continue
92
+ var_type = "global" if payload.get("type") == "global" else "local"
93
+ normalized[key] = {"value": payload.get("value"), "type": var_type}
94
+ return normalized
95
+
96
+ def update_vars_to_state_dict(self):
97
+ owner_state = self._resolve_variable_owner_state()
98
+ current = dict(owner_state.get("variables", {}))
99
+
100
+ for k in self._dirty_global_variables:
101
+ v = self.variables.get("global", {}).get(k)
102
+ current[k] = {"value": v, "type": "global"}
103
+
104
+ for k in self._dirty_local_variables:
105
+ v = self.variables.get("local", {}).get(k)
106
+ current[k] = {"value": v, "type": "local"}
107
+
108
+ owner_state["variables"] = current
109
+ # 로컬 state에도 미러링해 UI/호환성 유지
110
+ self.state_dict["variables"] = current
111
+ self._dirty_global_variables.clear()
112
+ self._dirty_local_variables.clear()
113
+
114
+ def sync_state_variables(self):
115
+ """owner(state 혹은 parent state)의 variables를 런타임 캐시에 반영"""
116
+ owner_state = self._resolve_variable_owner_state()
117
+ normalized = self._normalize_state_variables(owner_state.get("variables", {}))
118
+
119
+ self.variables["local"].clear()
120
+ self.variables["global"].clear()
121
+
122
+ for key, payload in normalized.items():
123
+ if payload["type"] == "global":
124
+ self.variables["global"][key] = payload["value"]
125
+ else:
126
+ self.variables["local"][key] = payload["value"]
127
+
128
+ self.state_dict["variables"] = dict(owner_state.get("variables", {}))
129
+
130
+ def update_local_variables(self, variables: dict[str, Any]):
131
+ normalized = t_to_dict(variables)
132
+ self.variables["local"].update(normalized)
133
+ self._dirty_local_variables.update(normalized.keys())
134
+ self.update_vars_to_state_dict()
135
+
136
+ def update_global_variables(self, variables: dict[str, Any]):
137
+ self.variables["global"].update(variables)
138
+ self._dirty_global_variables.update(variables.keys())
139
+ self.update_vars_to_state_dict()
140
+
141
+ def mark_dirty_variables(
142
+ self,
143
+ *,
144
+ local: set[str] | None = None,
145
+ global_: set[str] | None = None,
146
+ ):
147
+ self._dirty_local_variables.update(local or set())
148
+ self._dirty_global_variables.update(global_ or set())
149
+
150
+ def get_global_variable(self, var_name: str) -> Any:
151
+ robot_model = self.state_dict.get("robot_model", None)
152
+ category = self.state_dict.get("category", None)
153
+
154
+ if category is None or robot_model is None:
155
+ return None
156
+
157
+ try:
158
+ if category == "manipulate":
159
+ fn = self.get_sdk_function("rb_manipulate_sdk.get_data.get_variable")
160
+
161
+ res = fn(robot_model, var_name) if fn is not None else None
162
+
163
+ if res is None:
164
+ raise RuntimeError(f"Failed to get global variable: {var_name}")
165
+
166
+ self.update_global_variables({var_name: res})
167
+
168
+ return res
169
+ except Exception as e:
170
+ raise e
171
+
172
+ def _ensure_sdk_roots(self):
173
+ if self._sdk_roots is not None:
174
+ return
175
+
176
+ try:
177
+ category = self.state_dict.get("category", None)
178
+
179
+ rb_base_sdk = RBBaseSDK()
180
+ rb_manipulate_sdk: RBManipulateSDK | None = None
181
+ rb_amr_sdk: RBAmrSDK | None = None
182
+ rb_program_sdk = RBProgramSDK()
183
+
184
+ if category == "manipulate":
185
+ rb_manipulate_sdk = RBManipulateSDK()
186
+ elif category == "amr":
187
+ rb_amr_sdk = RBAmrSDK()
188
+
189
+ self._sdk_roots = {
190
+ "rb_base_sdk": rb_base_sdk,
191
+ "rb_program_sdk": rb_program_sdk,
192
+ "rb_manipulate_sdk": rb_manipulate_sdk,
193
+ "rb_amr_sdk": rb_amr_sdk,
194
+ }
195
+ except Exception as e:
196
+ raise RuntimeError(
197
+ f"SDK initialization failed in process {self.process_id}: {e}"
198
+ ) from e
199
+
200
+ def get_sdk_function(self, func_name: str) -> Callable | None:
201
+ if not func_name:
202
+ return None
203
+
204
+ cached = self.sdk_functions.get(func_name)
205
+ if cached is not None:
206
+ return cached
207
+
208
+ self._ensure_sdk_roots()
209
+ if self._sdk_roots is None:
210
+ return None
211
+
212
+ parts = func_name.split(".")
213
+ if len(parts) < 2:
214
+ return None
215
+
216
+ obj = self._sdk_roots.get(parts[0])
217
+ if obj is None:
218
+ return None
219
+
220
+ for part in parts[1:]:
221
+ if part.startswith("_") or not hasattr(obj, part):
222
+ return None
223
+ obj = getattr(obj, part)
224
+
225
+ if not callable(obj):
226
+ return None
227
+
228
+ self.sdk_functions[func_name] = obj
229
+ return obj
230
+
231
+ def step_barrier(self):
232
+ """step_mode면, 이 스텝 수행 후 일시정지하고
233
+ resume 신호 올 때까지 대기"""
234
+ if not self.step_mode or self.step_num < 2:
235
+ return
236
+
237
+ if self.stop_event.is_set():
238
+ self.state_dict["state"] = RB_Flow_Manager_ProgramState.STOPPED
239
+ raise StopExecution("Execution stopped by user")
240
+
241
+ # pause_event가 이미 set된 상태로 barrier에 들어와도
242
+ # executor/UI 상태가 RUNNING으로 남지 않도록 WAITING 이벤트를 보낸다.
243
+ if not self.pause_event.is_set():
244
+ self.pause_event.set()
245
+ self.emit_pause(self.state_dict["current_step_id"], is_wait=True)
246
+ self._wait_for_resume()
247
+
248
+ def push_args(self, mapping: dict[str, Any] | None):
249
+ self._arg_scope.append(mapping or {})
250
+
251
+ def pop_args(self):
252
+ if self._arg_scope:
253
+ self._arg_scope.pop()
254
+
255
+ def lookup(self, key: str) -> Any:
256
+ idx = len(self._arg_scope) - 2
257
+
258
+ while idx >= 0:
259
+ scope = self._arg_scope[idx]
260
+ if key in scope:
261
+ return scope[key]
262
+ idx -= 1
263
+ raise KeyError(f"parent pointer not found: {key}")
264
+
265
+ def pause(self, is_wait: bool = False):
266
+ """현재 스크립트를 일시정지"""
267
+ if not self.pause_event.is_set():
268
+ self.pause_event.set()
269
+ self.emit_pause(self.state_dict["current_step_id"], is_wait)
270
+
271
+ self._wait_for_resume()
272
+
273
+ def resume(self):
274
+ """현재 스크립트를 재개"""
275
+ if self.resume_event.is_set():
276
+ return
277
+
278
+ self.resume_event.set()
279
+ self.pause_event.clear()
280
+ self.state_dict["state"] = RB_Flow_Manager_ProgramState.RUNNING
281
+
282
+ def stop(self):
283
+ """현재 스크립트를 중지"""
284
+ if not self.stop_event.is_set():
285
+ self.stop_event.set()
286
+ self.emit_stop(self.state_dict["current_step_id"])
287
+
288
+ self.pause_event.clear()
289
+
290
+ self._wait_for_resume()
291
+
292
+ def _wait_for_resume(self):
293
+ if self.stop_event.is_set() and not self.state_dict.get("ignore_stop", False):
294
+ self.state_dict["state"] = RB_Flow_Manager_ProgramState.STOPPED
295
+ raise StopExecution("Execution stopped by user")
296
+
297
+ if self.state_dict.get("ignore_pause", False):
298
+ self.pause_event.clear()
299
+ self.resume_event.clear()
300
+ if self.state_dict.get("state") == RB_Flow_Manager_ProgramState.PAUSED:
301
+ self.state_dict["state"] = RB_Flow_Manager_ProgramState.RUNNING
302
+ return
303
+
304
+ if self.pause_event.is_set():
305
+ # resume이 이미 와 있으면 바로 통과 (레이스 방지)
306
+ if self.resume_event.is_set():
307
+ self.resume_event.clear()
308
+ self.pause_event.clear()
309
+ self.state_dict["state"] = RB_Flow_Manager_ProgramState.RUNNING
310
+ return
311
+
312
+ self.state_dict["state"] = RB_Flow_Manager_ProgramState.PAUSED
313
+
314
+ while True:
315
+ if self.stop_event.is_set() and not self.state_dict.get("ignore_stop", False):
316
+ self.pause_event.clear()
317
+ self.resume_event.clear()
318
+ self.state_dict["state"] = RB_Flow_Manager_ProgramState.STOPPED
319
+ raise StopExecution("Execution stopped by user")
320
+
321
+ if self.state_dict.get("ignore_pause", False):
322
+ self.pause_event.clear()
323
+ self.resume_event.clear()
324
+ self.state_dict["state"] = RB_Flow_Manager_ProgramState.RUNNING
325
+ break
326
+
327
+ if self.resume_event.wait(timeout=0.01):
328
+ self.resume_event.clear()
329
+ self.pause_event.clear()
330
+ if self.stop_event.is_set() and not self.state_dict.get("ignore_stop", False):
331
+ self.state_dict["state"] = RB_Flow_Manager_ProgramState.STOPPED
332
+ raise StopExecution("Execution stopped by user")
333
+ self.state_dict["state"] = RB_Flow_Manager_ProgramState.RUNNING
334
+ break
335
+
336
+ def check_stop(self):
337
+ """stop/pause/resume 명령 처리"""
338
+ self._wait_for_resume() # 즉시 멈춤/중지 반영
339
+
340
+ def emit_next(self, step_id: str):
341
+ """next 이벤트 발생"""
342
+ try:
343
+ self.result_queue.put_nowait(
344
+ {
345
+ "type": "next",
346
+ "process_id": self.process_id,
347
+ "step_id": step_id,
348
+ "ts": time.time(),
349
+ "generation": self._generation,
350
+ "error": None,
351
+ }
352
+ )
353
+ except queue.Full:
354
+ # 고빈도 next 이벤트는 큐 포화 시 드롭해 stop 반응성을 보장한다.
355
+ return
356
+
357
+ def emit_pause(self, step_id: str, is_wait: bool = False):
358
+ """pause 이벤트 발생"""
359
+ self.result_queue.put(
360
+ {
361
+ "type": "pause",
362
+ "process_id": self.process_id,
363
+ "step_id": step_id,
364
+ "is_wait": is_wait,
365
+ "parent_process_id": self.parent_process_id,
366
+ "ts": time.time(),
367
+ "generation": self._generation,
368
+ "error": None,
369
+ }
370
+ )
371
+
372
+ def emit_wait(self, step_id: str):
373
+ self.result_queue.put(
374
+ {
375
+ "type": "wait",
376
+ "process_id": self.process_id,
377
+ "step_id": step_id,
378
+ "parent_process_id": self.parent_process_id,
379
+ "ts": time.time(),
380
+ "generation": self._generation,
381
+ "error": None,
382
+ }
383
+ )
384
+
385
+ def emit_resume(self, step_id: str):
386
+ """resume 이벤트 발생"""
387
+ self.result_queue.put(
388
+ {
389
+ "type": "resume",
390
+ "process_id": self.process_id,
391
+ "step_id": step_id,
392
+ "parent_process_id": self.parent_process_id,
393
+ "ts": time.time(),
394
+ "generation": self._generation,
395
+ "error": None,
396
+ }
397
+ )
398
+
399
+ def emit_stop(self, step_id: str):
400
+ """stop 이벤트 발생"""
401
+ self.result_queue.put(
402
+ {
403
+ "type": "stop",
404
+ "process_id": self.process_id,
405
+ "step_id": step_id,
406
+ "parent_process_id": self.parent_process_id,
407
+ "ts": time.time(),
408
+ "generation": self._generation,
409
+ "error": None,
410
+ }
411
+ )
412
+
413
+ def emit_error(self, step_id: str, error: Exception):
414
+ """error 이벤트 발생"""
415
+
416
+ self.result_queue.put(
417
+ {
418
+ "type": "error",
419
+ "process_id": self.process_id,
420
+ "step_id": step_id,
421
+ "ts": time.time(),
422
+ "generation": self._generation,
423
+ "error": str(error),
424
+ }
425
+ )
426
+
427
+ def emit_subtask_sync_register(
428
+ self, sub_task_tree, post_tree=None, subtask_type: Literal["INSERT", "CHANGE"] = "INSERT"
429
+ ):
430
+ """서브태스크 sync flags 등록 요청 이벤트"""
431
+ self.result_queue.put(
432
+ {
433
+ "type": "subtask_sync_register",
434
+ "process_id": self.process_id,
435
+ "subtask_type": subtask_type,
436
+ "sub_task_tree": sub_task_tree.to_dict() if sub_task_tree else None,
437
+ "post_tree": post_tree.to_dict() if post_tree else None,
438
+ "ts": time.time(),
439
+ "generation": self._generation,
440
+ }
441
+ )
442
+
443
+ def emit_subtask_sync_unregister(
444
+ self, sub_task_tree, post_tree=None, subtask_type: Literal["INSERT", "CHANGE"] = "INSERT"
445
+ ):
446
+ """서브태스크 sync flags 해제 요청 이벤트"""
447
+ self.result_queue.put(
448
+ {
449
+ "type": "subtask_sync_unregister",
450
+ "process_id": self.process_id,
451
+ "subtask_type": subtask_type,
452
+ "sub_task_tree": sub_task_tree.to_dict() if sub_task_tree else None,
453
+ "post_tree": post_tree.to_dict() if post_tree else None,
454
+ "ts": time.time(),
455
+ "generation": self._generation,
456
+ }
457
+ )
458
+
459
+ def emit_main_tree_sync_unregister(self):
460
+ """메인 트리 sync flags 해제 요청 (CHANGE 타입 전환 시)"""
461
+ self.result_queue.put(
462
+ {
463
+ "type": "main_tree_sync_unregister",
464
+ "process_id": self.process_id,
465
+ "ts": time.time(),
466
+ "generation": self._generation,
467
+ }
468
+ )
469
+
470
+ def emit_sub_task_start(self, task_id: str, sub_task_type: Literal["INSERT", "CHANGE"]):
471
+ """sub_task_start 이벤트 발생"""
472
+ self.result_queue.put(
473
+ {
474
+ "type": "sub_task_start",
475
+ "process_id": self.process_id,
476
+ "sub_task_id": task_id,
477
+ "sub_task_type": sub_task_type,
478
+ "ts": time.time(),
479
+ "generation": self._generation,
480
+ "error": None,
481
+ }
482
+ )
483
+
484
+ def emit_sub_task_done(self, task_id: str, sub_task_type: Literal["INSERT", "CHANGE"]):
485
+ """sub_task_done 이벤트 발생"""
486
+ self.result_queue.put(
487
+ {
488
+ "type": "sub_task_done",
489
+ "process_id": self.process_id,
490
+ "sub_task_id": task_id,
491
+ "sub_task_type": sub_task_type,
492
+ "ts": time.time(),
493
+ "generation": self._generation,
494
+ "error": None,
495
+ }
496
+ )
497
+
498
+ def emit_event_sub_task_start(
499
+ self,
500
+ *,
501
+ event_task_id: str,
502
+ event_tree,
503
+ step_id: str | None = None,
504
+ run_mode: Literal["SYNC", "ASYNC"] = "ASYNC",
505
+ call_seq: int | None = None,
506
+ ):
507
+ """EventSubTask 실행 요청 이벤트"""
508
+ self.result_queue.put(
509
+ {
510
+ "type": "event_sub_task_start",
511
+ "process_id": self.process_id,
512
+ "event_task_id": event_task_id,
513
+ "event_tree": event_tree.to_dict() if event_tree is not None else None,
514
+ "step_id": step_id,
515
+ "run_mode": run_mode,
516
+ "call_seq": call_seq,
517
+ "ts": time.time(),
518
+ "generation": self._generation,
519
+ "error": None,
520
+ }
521
+ )
522
+
523
+ def emit_post_start(self):
524
+ """post_start 이벤트 발생"""
525
+ self.result_queue.put(
526
+ {
527
+ "type": "post_start",
528
+ "process_id": self.process_id,
529
+ "ts": time.time(),
530
+ "generation": self._generation,
531
+ "error": None,
532
+ }
533
+ )
534
+
535
+ def emit_done(self, step_id: str):
536
+ """done 이벤트 발생"""
537
+ try:
538
+ self.result_queue.put_nowait(
539
+ {
540
+ "type": "done",
541
+ "process_id": self.process_id,
542
+ "step_id": step_id,
543
+ "ts": time.time(),
544
+ "generation": self._generation,
545
+ "error": None,
546
+ }
547
+ )
548
+ except queue.Full:
549
+ # 고빈도 done 이벤트는 큐 포화 시 드롭해 stop 반응성을 보장한다.
550
+ return
551
+
552
+ def pause_all(self):
553
+ self.result_queue.put(
554
+ {
555
+ "type": "control",
556
+ "action": "pause_all",
557
+ "process_id": self.process_id,
558
+ "ts": time.time(),
559
+ "generation": self._generation,
560
+ }
561
+ )
562
+
563
+ def resume_all(self):
564
+ self.result_queue.put(
565
+ {
566
+ "type": "control",
567
+ "action": "resume_all",
568
+ "process_id": self.process_id,
569
+ "ts": time.time(),
570
+ "generation": self._generation,
571
+ }
572
+ )
573
+
574
+ def enter_folder(self):
575
+ """Folder 진입"""
576
+ self._folder_depth += 1
577
+
578
+ def leave_folder(self):
579
+ """Folder 탈출"""
580
+ self._folder_depth -= 1
581
+
582
+ def break_folder(self):
583
+ """Folder 실행 중단"""
584
+ if self._folder_depth > 0:
585
+ raise BreakFolder()
586
+
587
+ def halt_sub_task(self):
588
+ """서브 태스크 실행 중단"""
589
+ raise SubTaskHaltException()
590
+
591
+ def _safe_close_sdk(self):
592
+ """SDK 안전 종료"""
593
+ with contextlib.suppress(Exception):
594
+ if self._sdk_roots is not None:
595
+ for sdk in self._sdk_roots.values():
596
+ close_fn = getattr(sdk, "close", None)
597
+ if callable(close_fn):
598
+ close_fn()
599
+
600
+ def close(self):
601
+ """안전하게 close"""
602
+ self._safe_close_sdk()
603
+
604
+ def __del__(self):
605
+ """소멸자"""
606
+ if getattr(sys, "is_finalizing", lambda: False)():
607
+ return
608
+
609
+ with contextlib.suppress(builtins.BaseException):
610
+ self.close()
@@ -0,0 +1,88 @@
1
+ from abc import (
2
+ ABC,
3
+ abstractmethod,
4
+ )
5
+ from collections.abc import (
6
+ MutableMapping,
7
+ )
8
+ from typing import Any, Literal
9
+
10
+
11
+ class BaseController(ABC):
12
+ """모든 컨트롤러가 따라야 하는 공통 인터페이스"""
13
+
14
+ @abstractmethod
15
+ def on_init(self, state_dicts: dict[str, MutableMapping[str, Any]]) -> None:
16
+ """컨트롤러 초기화"""
17
+
18
+ @abstractmethod
19
+ def on_start(self, task_id: str) -> None:
20
+ """스크립트/플로우 시작 시점에 한 번 호출"""
21
+
22
+ @abstractmethod
23
+ def on_stop(self, task_id: str, step_id: str) -> None:
24
+ """각 Step이 Stop 될때 동작을 수행"""
25
+
26
+ @abstractmethod
27
+ def on_pause(self, task_id: str, step_id: str) -> None:
28
+ """각 Step이 Pause 될때 실제 동작을 수행"""
29
+
30
+ @abstractmethod
31
+ def on_wait(self, task_id: str, step_id: str) -> None:
32
+ """각 Step이 Wait 될때 실제 동작을 수행"""
33
+
34
+ @abstractmethod
35
+ def on_resume(self, task_id: str, step_id: str) -> None:
36
+ """각 Step이 Resume 될때 실제 동작을 수행"""
37
+
38
+ @abstractmethod
39
+ def on_next(self, task_id: str, step_id: str) -> None:
40
+ """각 Step이 다음 Step으로 이동 할때 동작을 수행"""
41
+
42
+ @abstractmethod
43
+ def on_sub_task_start(self, task_id: str, sub_task_id: str, sub_task_type: Literal["INSERT", "CHANGE"]) -> None:
44
+ """각 Step이 서브 태스크 시작 시점에 동작을 수행"""
45
+
46
+ @abstractmethod
47
+ def on_sub_task_done(self, task_id: str, sub_task_id: str, sub_task_type: Literal["INSERT", "CHANGE"]) -> None:
48
+ """각 Step이 서브 태스크 완료 시점에 동작을 수행"""
49
+
50
+ @abstractmethod
51
+ def on_done(self, task_id: str, step_id: str) -> None:
52
+ """각 Step이 완료 되었을때 동작을 수행"""
53
+
54
+ @abstractmethod
55
+ def on_error(self, task_id: str, step_id: str, error: Exception) -> None:
56
+ """각 Step에서 Error 발생 시 동작을 수행"""
57
+
58
+ @abstractmethod
59
+ def on_post_start(self, task_id: str) -> None:
60
+ """스크립트/플로우 Post Tree의 시작 시점에 동작을 수행"""
61
+
62
+ @abstractmethod
63
+ def on_complete(self, task_id: str) -> None:
64
+ """스크립트/플로우 완료 시점에 동작을 수행"""
65
+
66
+ @abstractmethod
67
+ def on_close(self) -> None:
68
+ """컨트롤러 종료"""
69
+
70
+ @abstractmethod
71
+ def on_all_complete(self) -> None:
72
+ """모든 스크립트/플로우 완료 시점에 동작을 수행"""
73
+
74
+ @abstractmethod
75
+ def on_all_stop(self) -> None:
76
+ """모든 스크립트/플로우 Stop 시점에 동작을 수행"""
77
+
78
+ @abstractmethod
79
+ def on_all_pause(self) -> None:
80
+ """모든 스크립트/플로우 Pause 시점에 동작을 수행"""
81
+
82
+ @abstractmethod
83
+ def on_all_wait(self) -> None:
84
+ """모든 스크립트/플로우 Wait 시점에 동작을 수행"""
85
+
86
+ @abstractmethod
87
+ def on_all_resume(self) -> None:
88
+ """모든 스크립트/플로우 Resume 시점에 동작을 수행"""