ice-rules 2.0.1__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.
- ice/__init__.py +66 -0
- ice/_internal/__init__.py +2 -0
- ice/_internal/executor.py +82 -0
- ice/_internal/timeutil.py +51 -0
- ice/_internal/uuid.py +80 -0
- ice/cache/__init__.py +6 -0
- ice/cache/conf_cache.py +347 -0
- ice/cache/handler_cache.py +207 -0
- ice/client/__init__.py +7 -0
- ice/client/async_file_client.py +397 -0
- ice/client/file_client.py +422 -0
- ice/context/__init__.py +8 -0
- ice/context/context.py +38 -0
- ice/context/pack.py +121 -0
- ice/context/roam.py +206 -0
- ice/dispatcher.py +202 -0
- ice/dto.py +102 -0
- ice/enums.py +57 -0
- ice/handler/__init__.py +6 -0
- ice/handler/handler.py +117 -0
- ice/leaf/__init__.py +6 -0
- ice/leaf/registry.py +467 -0
- ice/log.py +106 -0
- ice/node/__init__.py +8 -0
- ice/node/base.py +192 -0
- ice/node/leaf.py +15 -0
- ice/node/relation.py +53 -0
- ice/relation/__init__.py +12 -0
- ice/relation/parallel.py +226 -0
- ice/relation/serial.py +165 -0
- ice_rules-2.0.1.dist-info/METADATA +103 -0
- ice_rules-2.0.1.dist-info/RECORD +34 -0
- ice_rules-2.0.1.dist-info/WHEEL +5 -0
- ice_rules-2.0.1.dist-info/top_level.txt +1 -0
ice/__init__.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ice Rule Engine Python SDK
|
|
3
|
+
|
|
4
|
+
A Python implementation of the Ice rule engine, compatible with Java and Go SDKs.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from ice.context.roam import Roam
|
|
8
|
+
from ice.context.pack import Pack
|
|
9
|
+
from ice.context.context import Context
|
|
10
|
+
from ice.enums import RunState, NodeType, TimeType
|
|
11
|
+
from ice.leaf.registry import leaf, register_leaf, get_leaf_nodes, LeafMeta, IceField, IceIgnore, FieldMeta
|
|
12
|
+
from ice.log import Logger, set_logger
|
|
13
|
+
from ice.client.file_client import FileClient
|
|
14
|
+
from ice.client.async_file_client import AsyncFileClient
|
|
15
|
+
from ice.dispatcher import (
|
|
16
|
+
sync_process,
|
|
17
|
+
async_process,
|
|
18
|
+
process_ctx,
|
|
19
|
+
process_single_ctx,
|
|
20
|
+
process_roam,
|
|
21
|
+
process_single_roam,
|
|
22
|
+
get_handler_by_id,
|
|
23
|
+
get_handlers_by_scene,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
from importlib.metadata import version
|
|
28
|
+
__version__ = version("ice-rules")
|
|
29
|
+
except Exception:
|
|
30
|
+
__version__ = "0.0.0" # fallback for development
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
# Context
|
|
34
|
+
"Roam",
|
|
35
|
+
"Pack",
|
|
36
|
+
"Context",
|
|
37
|
+
# Enums
|
|
38
|
+
"RunState",
|
|
39
|
+
"NodeType",
|
|
40
|
+
"TimeType",
|
|
41
|
+
# Leaf registration
|
|
42
|
+
"leaf",
|
|
43
|
+
"register_leaf",
|
|
44
|
+
"get_leaf_nodes",
|
|
45
|
+
"IceField",
|
|
46
|
+
"IceIgnore",
|
|
47
|
+
"LeafMeta",
|
|
48
|
+
# Logging
|
|
49
|
+
"Logger",
|
|
50
|
+
"set_logger",
|
|
51
|
+
# Clients
|
|
52
|
+
"FileClient",
|
|
53
|
+
"AsyncFileClient",
|
|
54
|
+
# Processing (main)
|
|
55
|
+
"sync_process",
|
|
56
|
+
"async_process",
|
|
57
|
+
# Processing (convenience - matching Java/Go)
|
|
58
|
+
"process_ctx",
|
|
59
|
+
"process_single_ctx",
|
|
60
|
+
"process_roam",
|
|
61
|
+
"process_single_roam",
|
|
62
|
+
# Handler access (matching Go)
|
|
63
|
+
"get_handler_by_id",
|
|
64
|
+
"get_handlers_by_scene",
|
|
65
|
+
]
|
|
66
|
+
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Thread pool executor for parallel nodes in Ice SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from concurrent.futures import ThreadPoolExecutor, Future
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from ice.context.context import Context
|
|
12
|
+
from ice.node.base import Node
|
|
13
|
+
from ice.enums import RunState
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Global executor
|
|
17
|
+
_executor: ThreadPoolExecutor | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def init_executor(parallelism: int = -1) -> None:
|
|
21
|
+
"""
|
|
22
|
+
Initialize the global thread pool executor.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
parallelism: Number of worker threads. -1 means use CPU count.
|
|
26
|
+
"""
|
|
27
|
+
global _executor
|
|
28
|
+
if _executor is not None:
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
if parallelism <= 0:
|
|
32
|
+
parallelism = os.cpu_count() or 4
|
|
33
|
+
|
|
34
|
+
_executor = ThreadPoolExecutor(max_workers=parallelism, thread_name_prefix="ice-worker")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def shutdown_executor() -> None:
|
|
38
|
+
"""Shutdown the global executor."""
|
|
39
|
+
global _executor
|
|
40
|
+
if _executor is not None:
|
|
41
|
+
_executor.shutdown(wait=True)
|
|
42
|
+
_executor = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_executor() -> ThreadPoolExecutor:
|
|
46
|
+
"""Get the global executor, initializing if needed."""
|
|
47
|
+
global _executor
|
|
48
|
+
if _executor is None:
|
|
49
|
+
init_executor()
|
|
50
|
+
return _executor # type: ignore
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class NodeResult:
|
|
55
|
+
"""Result of a node execution."""
|
|
56
|
+
state: RunState
|
|
57
|
+
error: Exception | None = None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def submit_node(node: Node, ctx: Context) -> Future[NodeResult]:
|
|
61
|
+
"""
|
|
62
|
+
Submit a node for execution in the thread pool.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
node: The node to execute
|
|
66
|
+
ctx: The execution context (should be a copy for parallel execution)
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
A Future containing the NodeResult
|
|
70
|
+
"""
|
|
71
|
+
from ice.enums import RunState
|
|
72
|
+
|
|
73
|
+
def execute() -> NodeResult:
|
|
74
|
+
try:
|
|
75
|
+
state = node.process(ctx)
|
|
76
|
+
return NodeResult(state=state)
|
|
77
|
+
except Exception as e:
|
|
78
|
+
return NodeResult(state=RunState.SHUT_DOWN, error=e)
|
|
79
|
+
|
|
80
|
+
executor = get_executor()
|
|
81
|
+
return executor.submit(execute)
|
|
82
|
+
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Time utilities for Ice SDK."""
|
|
2
|
+
|
|
3
|
+
from ice.enums import TimeType
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def time_disabled(
|
|
7
|
+
time_type: TimeType | int,
|
|
8
|
+
request_time: int,
|
|
9
|
+
start: int,
|
|
10
|
+
end: int,
|
|
11
|
+
) -> bool:
|
|
12
|
+
"""
|
|
13
|
+
Check if the node should be disabled based on time constraints.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
time_type: Time control type
|
|
17
|
+
request_time: Current request time in milliseconds
|
|
18
|
+
start: Start time in milliseconds
|
|
19
|
+
end: End time in milliseconds
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
True if the node should be disabled, False otherwise
|
|
23
|
+
"""
|
|
24
|
+
if isinstance(time_type, int):
|
|
25
|
+
time_type = TimeType(time_type) if time_type in TimeType._value2member_map_ else TimeType.NONE
|
|
26
|
+
|
|
27
|
+
if time_type == TimeType.NONE:
|
|
28
|
+
return False
|
|
29
|
+
|
|
30
|
+
if time_type == TimeType.BETWEEN:
|
|
31
|
+
# Must be between start and end
|
|
32
|
+
if start > 0 and request_time < start:
|
|
33
|
+
return True
|
|
34
|
+
if end > 0 and request_time > end:
|
|
35
|
+
return True
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
if time_type == TimeType.AFTER_START:
|
|
39
|
+
# Must be after start
|
|
40
|
+
if start > 0 and request_time < start:
|
|
41
|
+
return True
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
if time_type == TimeType.BEFORE_END:
|
|
45
|
+
# Must be before end
|
|
46
|
+
if end > 0 and request_time > end:
|
|
47
|
+
return True
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
return False
|
|
51
|
+
|
ice/_internal/uuid.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""UUID utilities for Ice SDK."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import secrets
|
|
5
|
+
import uuid as uuid_module
|
|
6
|
+
|
|
7
|
+
# 64 character alphabet (same as Java: A-Z, a-z, 0-9, -, _)
|
|
8
|
+
_DIGITS64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def generate_uuid22() -> str:
|
|
12
|
+
"""Generate a 22-character UUID (base64 encoded UUID without padding)."""
|
|
13
|
+
u = uuid_module.uuid4()
|
|
14
|
+
return base64.urlsafe_b64encode(u.bytes).decode("ascii").rstrip("=")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def generate_uuid() -> str:
|
|
18
|
+
"""Generate a standard UUID string."""
|
|
19
|
+
return str(uuid_module.uuid4())
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def generate_short_id() -> str:
|
|
23
|
+
"""
|
|
24
|
+
Generate an 11-character short ID (same format as Java).
|
|
25
|
+
Uses 8 random bytes encoded with base64 variant to produce 11 characters.
|
|
26
|
+
"""
|
|
27
|
+
random_bytes = bytearray(secrets.token_bytes(8))
|
|
28
|
+
|
|
29
|
+
# Set version 4 marker (same as Java)
|
|
30
|
+
random_bytes[6] = (random_bytes[6] & 0x0f) | 0x40
|
|
31
|
+
|
|
32
|
+
# Convert bytes to int64
|
|
33
|
+
msb = 0
|
|
34
|
+
for i in range(8):
|
|
35
|
+
msb = (msb << 8) | (random_bytes[i] & 0xff)
|
|
36
|
+
|
|
37
|
+
# Encode to 11 characters using base64 variant
|
|
38
|
+
out = [''] * 12
|
|
39
|
+
bit = 0
|
|
40
|
+
bt1 = 8
|
|
41
|
+
bt2 = 8
|
|
42
|
+
offsetm = 1
|
|
43
|
+
idx = 0
|
|
44
|
+
|
|
45
|
+
while offsetm > 0:
|
|
46
|
+
offsetm = 64 - ((bit + 3) << 3)
|
|
47
|
+
|
|
48
|
+
if bt1 > 3:
|
|
49
|
+
mask = (1 << (8 * 3)) - 1
|
|
50
|
+
elif bt1 >= 0:
|
|
51
|
+
mask = (1 << (8 * bt1)) - 1
|
|
52
|
+
bt2 -= 3 - bt1
|
|
53
|
+
else:
|
|
54
|
+
min_bt = min(bt2, 3)
|
|
55
|
+
mask = (1 << (8 * min_bt)) - 1
|
|
56
|
+
bt2 -= 3
|
|
57
|
+
|
|
58
|
+
tmp = 0
|
|
59
|
+
if bt1 > 0:
|
|
60
|
+
bt1 -= 3
|
|
61
|
+
if offsetm < 0:
|
|
62
|
+
tmp = msb
|
|
63
|
+
else:
|
|
64
|
+
tmp = (msb >> offsetm) & mask
|
|
65
|
+
if bt1 < 0:
|
|
66
|
+
tmp <<= abs(offsetm)
|
|
67
|
+
|
|
68
|
+
out[idx + 3] = _DIGITS64[tmp & 0x3f]
|
|
69
|
+
tmp >>= 6
|
|
70
|
+
out[idx + 2] = _DIGITS64[tmp & 0x3f]
|
|
71
|
+
tmp >>= 6
|
|
72
|
+
out[idx + 1] = _DIGITS64[tmp & 0x3f]
|
|
73
|
+
tmp >>= 6
|
|
74
|
+
out[idx] = _DIGITS64[tmp & 0x3f]
|
|
75
|
+
|
|
76
|
+
bit += 3
|
|
77
|
+
idx += 4
|
|
78
|
+
|
|
79
|
+
return ''.join(out[:11])
|
|
80
|
+
|
ice/cache/__init__.py
ADDED
ice/cache/conf_cache.py
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
"""Configuration cache for Ice SDK.
|
|
2
|
+
|
|
3
|
+
This module mirrors Java's IceConfCache logic for consistency.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import threading
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from ice.dto import ConfDto
|
|
12
|
+
from ice.enums import NodeType, RunState, TimeType
|
|
13
|
+
from ice.node.base import Base, Node
|
|
14
|
+
from ice.node.relation import Relation
|
|
15
|
+
from ice.relation.serial import And, Any, All, TrueNode, NoneNode
|
|
16
|
+
from ice.relation.parallel import ParallelAnd, ParallelAny, ParallelAll, ParallelTrue, ParallelNone
|
|
17
|
+
from ice.leaf.registry import create_leaf_node, set_leaf_base
|
|
18
|
+
from ice import log
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Global caches with locks
|
|
25
|
+
_conf_map: dict[int, Node] = {}
|
|
26
|
+
_parent_ids_map: dict[int, set[int]] = {}
|
|
27
|
+
_forward_use_ids_map: dict[int, set[int]] = {}
|
|
28
|
+
_conf_lock = threading.RLock()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_conf(conf_id: int) -> Node | None:
|
|
32
|
+
"""Get a node by its configuration ID."""
|
|
33
|
+
with _conf_lock:
|
|
34
|
+
return _conf_map.get(conf_id)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_conf_map() -> dict[int, Node]:
|
|
38
|
+
"""Get a copy of the conf map."""
|
|
39
|
+
with _conf_lock:
|
|
40
|
+
return _conf_map.copy()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def insert_or_update_confs(confs: list[ConfDto]) -> list[str]:
|
|
44
|
+
"""Insert or update configurations.
|
|
45
|
+
|
|
46
|
+
This method mirrors Java's IceConfCache.insertOrUpdate logic.
|
|
47
|
+
Returns a list of error messages.
|
|
48
|
+
"""
|
|
49
|
+
if not confs:
|
|
50
|
+
return []
|
|
51
|
+
|
|
52
|
+
errors: list[str] = []
|
|
53
|
+
|
|
54
|
+
with _conf_lock:
|
|
55
|
+
# Build temporary map for new nodes
|
|
56
|
+
tmp_conf_map: dict[int, Node] = {}
|
|
57
|
+
|
|
58
|
+
for conf_dto in confs:
|
|
59
|
+
try:
|
|
60
|
+
node = _convert_node(conf_dto)
|
|
61
|
+
if node:
|
|
62
|
+
tmp_conf_map[conf_dto.id] = node
|
|
63
|
+
except Exception as e:
|
|
64
|
+
errors.append(f"failed to convert node: {e}, confId: {conf_dto.id}")
|
|
65
|
+
log.error("failed to convert node", error=str(e), confId=conf_dto.id)
|
|
66
|
+
|
|
67
|
+
# Set up relationships and handle cleanup of old relationships
|
|
68
|
+
for conf_dto in confs:
|
|
69
|
+
origin = _conf_map.get(conf_dto.id)
|
|
70
|
+
is_relation = NodeType(conf_dto.type).is_relation() if conf_dto.type in NodeType._value2member_map_ else False
|
|
71
|
+
|
|
72
|
+
node = tmp_conf_map.get(conf_dto.id)
|
|
73
|
+
if node is None:
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
# Parse new sonIds
|
|
77
|
+
son_ids: list[int] = []
|
|
78
|
+
son_id_set: set[int] = set()
|
|
79
|
+
if is_relation and conf_dto.sonIds:
|
|
80
|
+
son_ids = _parse_son_ids(conf_dto.sonIds)
|
|
81
|
+
son_id_set = set(son_ids)
|
|
82
|
+
|
|
83
|
+
# Handle relation node setup
|
|
84
|
+
if is_relation and isinstance(node, Relation):
|
|
85
|
+
if son_ids:
|
|
86
|
+
children: list[Node] = []
|
|
87
|
+
for son_id in son_ids:
|
|
88
|
+
# Track parent-child relationships
|
|
89
|
+
if son_id not in _parent_ids_map:
|
|
90
|
+
_parent_ids_map[son_id] = set()
|
|
91
|
+
_parent_ids_map[son_id].add(conf_dto.id)
|
|
92
|
+
|
|
93
|
+
child = tmp_conf_map.get(son_id) or _conf_map.get(son_id)
|
|
94
|
+
if child is None:
|
|
95
|
+
errors.append(f"sonId not exist: {son_id}")
|
|
96
|
+
log.error("sonId not exist", sonId=son_id)
|
|
97
|
+
else:
|
|
98
|
+
children.append(child)
|
|
99
|
+
node.set_children(children)
|
|
100
|
+
node.set_son_ids(son_ids)
|
|
101
|
+
|
|
102
|
+
# Clean up old parent-child relationships (Java lines 108-125)
|
|
103
|
+
if origin is not None and isinstance(origin, Relation):
|
|
104
|
+
origin_children = origin.get_children()
|
|
105
|
+
if origin_children:
|
|
106
|
+
for son_node in origin_children:
|
|
107
|
+
if son_node is not None:
|
|
108
|
+
son_node_id = son_node.get_node_id()
|
|
109
|
+
if son_node_id not in son_id_set:
|
|
110
|
+
# Child no longer in sonIds, remove parent reference
|
|
111
|
+
parent_ids = _parent_ids_map.get(son_node_id)
|
|
112
|
+
if parent_ids:
|
|
113
|
+
parent_ids.discard(conf_dto.id)
|
|
114
|
+
else:
|
|
115
|
+
# Current node is NOT a relation node
|
|
116
|
+
# If origin was a relation node, clean up all old children's parent references (Java lines 126-145)
|
|
117
|
+
if origin is not None and isinstance(origin, Relation):
|
|
118
|
+
origin_children = origin.get_children()
|
|
119
|
+
if origin_children:
|
|
120
|
+
for son_node in origin_children:
|
|
121
|
+
if son_node is not None:
|
|
122
|
+
parent_ids = _parent_ids_map.get(son_node.get_node_id())
|
|
123
|
+
if parent_ids:
|
|
124
|
+
parent_ids.discard(conf_dto.id)
|
|
125
|
+
|
|
126
|
+
# Handle forward node cleanup and setup (Java lines 146-176)
|
|
127
|
+
# First, clean up old forward reference if it changed
|
|
128
|
+
if origin is not None:
|
|
129
|
+
old_forward = _get_forward(origin)
|
|
130
|
+
if old_forward is not None:
|
|
131
|
+
old_forward_id = old_forward.get_node_id()
|
|
132
|
+
# If new forwardId is different or not set, remove old reference
|
|
133
|
+
if conf_dto.forwardId == 0 or conf_dto.forwardId != old_forward_id:
|
|
134
|
+
forward_use_ids = _forward_use_ids_map.get(old_forward_id)
|
|
135
|
+
if forward_use_ids:
|
|
136
|
+
forward_use_ids.discard(conf_dto.id)
|
|
137
|
+
|
|
138
|
+
# Set up new forward reference
|
|
139
|
+
if conf_dto.forwardId > 0:
|
|
140
|
+
if conf_dto.forwardId not in _forward_use_ids_map:
|
|
141
|
+
_forward_use_ids_map[conf_dto.forwardId] = set()
|
|
142
|
+
_forward_use_ids_map[conf_dto.forwardId].add(conf_dto.id)
|
|
143
|
+
|
|
144
|
+
forward = tmp_conf_map.get(conf_dto.forwardId) or _conf_map.get(conf_dto.forwardId)
|
|
145
|
+
if forward is None:
|
|
146
|
+
errors.append(f"forwardId not exist: {conf_dto.forwardId}")
|
|
147
|
+
log.error("forwardId not exist", forwardId=conf_dto.forwardId)
|
|
148
|
+
else:
|
|
149
|
+
_set_forward(node, forward)
|
|
150
|
+
|
|
151
|
+
# Update global map
|
|
152
|
+
_conf_map.update(tmp_conf_map)
|
|
153
|
+
|
|
154
|
+
# Update parent nodes' children lists (Java lines 179-226)
|
|
155
|
+
for conf_dto in confs:
|
|
156
|
+
conf_id = conf_dto.id
|
|
157
|
+
|
|
158
|
+
# Update parents' children lists
|
|
159
|
+
parent_ids = _parent_ids_map.get(conf_id)
|
|
160
|
+
if parent_ids:
|
|
161
|
+
remove_parent_ids: list[int] = []
|
|
162
|
+
for parent_id in list(parent_ids):
|
|
163
|
+
parent = _conf_map.get(parent_id)
|
|
164
|
+
if parent is None:
|
|
165
|
+
errors.append(f"parentId not exist: {parent_id}")
|
|
166
|
+
log.error("parentId not exist", parentId=parent_id)
|
|
167
|
+
continue
|
|
168
|
+
if isinstance(parent, Relation):
|
|
169
|
+
son_ids = parent.get_son_ids()
|
|
170
|
+
if son_ids:
|
|
171
|
+
new_children = []
|
|
172
|
+
for son_id in son_ids:
|
|
173
|
+
child = _conf_map.get(son_id)
|
|
174
|
+
if child:
|
|
175
|
+
new_children.append(child)
|
|
176
|
+
parent.set_children(new_children)
|
|
177
|
+
else:
|
|
178
|
+
# Parent is no longer a relation node, mark for removal
|
|
179
|
+
remove_parent_ids.append(parent_id)
|
|
180
|
+
# Remove invalid parent references
|
|
181
|
+
for pid in remove_parent_ids:
|
|
182
|
+
parent_ids.discard(pid)
|
|
183
|
+
|
|
184
|
+
# Update forward users
|
|
185
|
+
forward_use_ids = _forward_use_ids_map.get(conf_id)
|
|
186
|
+
if forward_use_ids:
|
|
187
|
+
for forward_use_id in list(forward_use_ids):
|
|
188
|
+
user = _conf_map.get(forward_use_id)
|
|
189
|
+
if user is None:
|
|
190
|
+
errors.append(f"forwardUseId not exist: {forward_use_id}")
|
|
191
|
+
log.error("forwardUseId not exist", forwardUseId=forward_use_id)
|
|
192
|
+
continue
|
|
193
|
+
forward_node = _conf_map.get(conf_id)
|
|
194
|
+
if forward_node:
|
|
195
|
+
_set_forward(user, forward_node)
|
|
196
|
+
|
|
197
|
+
# Update handler roots
|
|
198
|
+
tmp_node = _conf_map.get(conf_id)
|
|
199
|
+
if tmp_node:
|
|
200
|
+
from ice.cache.handler_cache import update_handler_root
|
|
201
|
+
update_handler_root(tmp_node)
|
|
202
|
+
|
|
203
|
+
return errors
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def delete_confs(conf_ids: list[int]) -> None:
|
|
207
|
+
"""Delete configurations by IDs."""
|
|
208
|
+
if not conf_ids:
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
with _conf_lock:
|
|
212
|
+
for conf_id in conf_ids:
|
|
213
|
+
if conf_id in _conf_map:
|
|
214
|
+
del _conf_map[conf_id]
|
|
215
|
+
if conf_id in _parent_ids_map:
|
|
216
|
+
del _parent_ids_map[conf_id]
|
|
217
|
+
if conf_id in _forward_use_ids_map:
|
|
218
|
+
del _forward_use_ids_map[conf_id]
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def clear_all() -> None:
|
|
222
|
+
"""Clear all caches."""
|
|
223
|
+
with _conf_lock:
|
|
224
|
+
_conf_map.clear()
|
|
225
|
+
_parent_ids_map.clear()
|
|
226
|
+
_forward_use_ids_map.clear()
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _convert_node(conf_dto: ConfDto) -> Node | None:
|
|
230
|
+
"""Convert a ConfDto to a Node."""
|
|
231
|
+
node_type = NodeType(conf_dto.type) if conf_dto.type in NodeType._value2member_map_ else None
|
|
232
|
+
|
|
233
|
+
if node_type is None:
|
|
234
|
+
return None
|
|
235
|
+
|
|
236
|
+
node: Node | None = None
|
|
237
|
+
|
|
238
|
+
# Create node based on type
|
|
239
|
+
if node_type == NodeType.NONE:
|
|
240
|
+
node = NoneNode()
|
|
241
|
+
elif node_type == NodeType.AND:
|
|
242
|
+
node = And()
|
|
243
|
+
elif node_type == NodeType.TRUE:
|
|
244
|
+
node = TrueNode()
|
|
245
|
+
elif node_type == NodeType.ALL:
|
|
246
|
+
node = All()
|
|
247
|
+
elif node_type == NodeType.ANY:
|
|
248
|
+
node = Any()
|
|
249
|
+
elif node_type == NodeType.P_NONE:
|
|
250
|
+
node = ParallelNone()
|
|
251
|
+
elif node_type == NodeType.P_AND:
|
|
252
|
+
node = ParallelAnd()
|
|
253
|
+
elif node_type == NodeType.P_TRUE:
|
|
254
|
+
node = ParallelTrue()
|
|
255
|
+
elif node_type == NodeType.P_ALL:
|
|
256
|
+
node = ParallelAll()
|
|
257
|
+
elif node_type == NodeType.P_ANY:
|
|
258
|
+
node = ParallelAny()
|
|
259
|
+
elif node_type in (NodeType.LEAF_FLOW, NodeType.LEAF_RESULT, NodeType.LEAF_NONE):
|
|
260
|
+
if conf_dto.confName:
|
|
261
|
+
node = create_leaf_node(conf_dto.confName, conf_dto.confField)
|
|
262
|
+
else:
|
|
263
|
+
# Try to create as leaf
|
|
264
|
+
if conf_dto.confName:
|
|
265
|
+
node = create_leaf_node(conf_dto.confName, conf_dto.confField)
|
|
266
|
+
|
|
267
|
+
# Set common properties
|
|
268
|
+
if node is not None:
|
|
269
|
+
_set_node_properties(node, conf_dto)
|
|
270
|
+
|
|
271
|
+
return node
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _set_node_properties(node: Node, conf_dto: ConfDto) -> None:
|
|
275
|
+
"""Set common node properties from ConfDto."""
|
|
276
|
+
base = Base()
|
|
277
|
+
base.ice_node_id = conf_dto.id
|
|
278
|
+
base.ice_node_debug = conf_dto.debug in (0, 1)
|
|
279
|
+
base.ice_inverse = conf_dto.inverse
|
|
280
|
+
base.ice_time_type = TimeType(conf_dto.timeType) if conf_dto.timeType in TimeType._value2member_map_ else TimeType.NONE
|
|
281
|
+
base.ice_start = conf_dto.start
|
|
282
|
+
base.ice_end = conf_dto.end
|
|
283
|
+
if conf_dto.errorState != 0 and conf_dto.errorState in RunState._value2member_map_:
|
|
284
|
+
base.ice_error_state = RunState(conf_dto.errorState)
|
|
285
|
+
base.ice_type = NodeType(conf_dto.type) if conf_dto.type in NodeType._value2member_map_ else NodeType.LEAF_NONE
|
|
286
|
+
|
|
287
|
+
# Set log name
|
|
288
|
+
if isinstance(node, Relation):
|
|
289
|
+
# Already set in constructor
|
|
290
|
+
pass
|
|
291
|
+
else:
|
|
292
|
+
base.ice_log_name = _get_simple_name(conf_dto.confName)
|
|
293
|
+
|
|
294
|
+
# Apply base properties
|
|
295
|
+
if isinstance(node, Relation):
|
|
296
|
+
node.ice_node_id = base.ice_node_id
|
|
297
|
+
node.ice_node_debug = base.ice_node_debug
|
|
298
|
+
node.ice_inverse = base.ice_inverse
|
|
299
|
+
node.ice_time_type = base.ice_time_type
|
|
300
|
+
node.ice_start = base.ice_start
|
|
301
|
+
node.ice_end = base.ice_end
|
|
302
|
+
node.ice_error_state = base.ice_error_state
|
|
303
|
+
node.ice_type = base.ice_type
|
|
304
|
+
else:
|
|
305
|
+
set_leaf_base(node, base)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _set_forward(node: Node, forward: Node | None) -> None:
|
|
309
|
+
"""Set the forward node on any node type."""
|
|
310
|
+
if hasattr(node, "set_forward"):
|
|
311
|
+
node.set_forward(forward)
|
|
312
|
+
elif hasattr(node, "ice_forward"):
|
|
313
|
+
node.ice_forward = forward
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _get_forward(node: Node) -> Node | None:
|
|
317
|
+
"""Get the forward node from any node type."""
|
|
318
|
+
if hasattr(node, "get_forward"):
|
|
319
|
+
return node.get_forward()
|
|
320
|
+
elif hasattr(node, "ice_forward"):
|
|
321
|
+
return node.ice_forward
|
|
322
|
+
return None
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _parse_son_ids(son_ids_str: str) -> list[int]:
|
|
326
|
+
"""Parse comma-separated son IDs string."""
|
|
327
|
+
if not son_ids_str:
|
|
328
|
+
return []
|
|
329
|
+
result = []
|
|
330
|
+
for s in son_ids_str.split(","):
|
|
331
|
+
s = s.strip()
|
|
332
|
+
if s:
|
|
333
|
+
try:
|
|
334
|
+
result.append(int(s))
|
|
335
|
+
except ValueError:
|
|
336
|
+
pass
|
|
337
|
+
return result
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _get_simple_name(full_name: str) -> str:
|
|
341
|
+
"""Extract the simple class name from a fully qualified name."""
|
|
342
|
+
if not full_name:
|
|
343
|
+
return ""
|
|
344
|
+
idx = full_name.rfind(".")
|
|
345
|
+
if idx >= 0:
|
|
346
|
+
return full_name[idx + 1:]
|
|
347
|
+
return full_name
|