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 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,2 @@
1
+ """Internal utilities for Ice SDK."""
2
+
@@ -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
@@ -0,0 +1,6 @@
1
+ """Cache module for Ice SDK."""
2
+
3
+ from ice.cache import conf_cache
4
+ from ice.cache import handler_cache
5
+
6
+ __all__ = ["conf_cache", "handler_cache"]
@@ -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