ice-rules 2.0.1__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.
- ice_rules-2.0.1/PKG-INFO +103 -0
- ice_rules-2.0.1/README.md +83 -0
- ice_rules-2.0.1/pyproject.toml +35 -0
- ice_rules-2.0.1/setup.cfg +4 -0
- ice_rules-2.0.1/src/ice/__init__.py +66 -0
- ice_rules-2.0.1/src/ice/_internal/__init__.py +2 -0
- ice_rules-2.0.1/src/ice/_internal/executor.py +82 -0
- ice_rules-2.0.1/src/ice/_internal/timeutil.py +51 -0
- ice_rules-2.0.1/src/ice/_internal/uuid.py +80 -0
- ice_rules-2.0.1/src/ice/cache/__init__.py +6 -0
- ice_rules-2.0.1/src/ice/cache/conf_cache.py +347 -0
- ice_rules-2.0.1/src/ice/cache/handler_cache.py +207 -0
- ice_rules-2.0.1/src/ice/client/__init__.py +7 -0
- ice_rules-2.0.1/src/ice/client/async_file_client.py +397 -0
- ice_rules-2.0.1/src/ice/client/file_client.py +422 -0
- ice_rules-2.0.1/src/ice/context/__init__.py +8 -0
- ice_rules-2.0.1/src/ice/context/context.py +38 -0
- ice_rules-2.0.1/src/ice/context/pack.py +121 -0
- ice_rules-2.0.1/src/ice/context/roam.py +206 -0
- ice_rules-2.0.1/src/ice/dispatcher.py +202 -0
- ice_rules-2.0.1/src/ice/dto.py +102 -0
- ice_rules-2.0.1/src/ice/enums.py +57 -0
- ice_rules-2.0.1/src/ice/handler/__init__.py +6 -0
- ice_rules-2.0.1/src/ice/handler/handler.py +117 -0
- ice_rules-2.0.1/src/ice/leaf/__init__.py +6 -0
- ice_rules-2.0.1/src/ice/leaf/registry.py +467 -0
- ice_rules-2.0.1/src/ice/log.py +106 -0
- ice_rules-2.0.1/src/ice/node/__init__.py +8 -0
- ice_rules-2.0.1/src/ice/node/base.py +192 -0
- ice_rules-2.0.1/src/ice/node/leaf.py +15 -0
- ice_rules-2.0.1/src/ice/node/relation.py +53 -0
- ice_rules-2.0.1/src/ice/relation/__init__.py +12 -0
- ice_rules-2.0.1/src/ice/relation/parallel.py +226 -0
- ice_rules-2.0.1/src/ice/relation/serial.py +165 -0
- ice_rules-2.0.1/src/ice_rules.egg-info/PKG-INFO +103 -0
- ice_rules-2.0.1/src/ice_rules.egg-info/SOURCES.txt +40 -0
- ice_rules-2.0.1/src/ice_rules.egg-info/dependency_links.txt +1 -0
- ice_rules-2.0.1/src/ice_rules.egg-info/top_level.txt +1 -0
- ice_rules-2.0.1/tests/__init__.py +2 -0
- ice_rules-2.0.1/tests/test_context.py +176 -0
- ice_rules-2.0.1/tests/test_leaf.py +142 -0
- ice_rules-2.0.1/tests/test_relation.py +172 -0
ice_rules-2.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ice-rules
|
|
3
|
+
Version: 2.0.1
|
|
4
|
+
Summary: Ice Rule Engine Python SDK
|
|
5
|
+
Author: waitmoon
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/waitmoon/ice
|
|
8
|
+
Project-URL: Documentation, https://waitmoon.github.io/ice
|
|
9
|
+
Project-URL: Repository, https://github.com/waitmoon/ice
|
|
10
|
+
Keywords: ice,rule-engine,decision-engine
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# Ice Python SDK
|
|
22
|
+
|
|
23
|
+
Ice 规则引擎的 Python 实现,与 Java ice-core 和 Go SDK 功能一致。
|
|
24
|
+
|
|
25
|
+
## 安装
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install ice-rules
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## 快速开始
|
|
32
|
+
|
|
33
|
+
### 1. 注册叶子节点
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import ice
|
|
37
|
+
from ice import Roam
|
|
38
|
+
|
|
39
|
+
@ice.leaf("com.example.ScoreFlow")
|
|
40
|
+
class ScoreFlow:
|
|
41
|
+
threshold: int = 0
|
|
42
|
+
|
|
43
|
+
def do_roam_flow(self, roam: Roam) -> bool:
|
|
44
|
+
return roam.get_int("score", 0) >= self.threshold
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 2. 启动客户端
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
# 同步方式
|
|
51
|
+
client = ice.FileClient(app=1, storage_path="./ice-data")
|
|
52
|
+
client.start()
|
|
53
|
+
|
|
54
|
+
pack = ice.Pack(ice_id=1)
|
|
55
|
+
pack.roam.put("score", 85)
|
|
56
|
+
results = ice.sync_process(pack)
|
|
57
|
+
|
|
58
|
+
client.destroy()
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 3. 异步方式
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
import asyncio
|
|
65
|
+
|
|
66
|
+
async def main():
|
|
67
|
+
client = ice.AsyncFileClient(app=1, storage_path="./ice-data")
|
|
68
|
+
await client.start()
|
|
69
|
+
|
|
70
|
+
pack = ice.Pack(ice_id=1)
|
|
71
|
+
pack.roam.put("score", 85)
|
|
72
|
+
results = await ice.async_process(pack)
|
|
73
|
+
|
|
74
|
+
await client.destroy()
|
|
75
|
+
|
|
76
|
+
asyncio.run(main())
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## 叶子节点类型
|
|
80
|
+
|
|
81
|
+
### Flow 类型(返回 True/False)
|
|
82
|
+
- `do_flow(ctx: Context) -> bool`
|
|
83
|
+
- `do_pack_flow(pack: Pack) -> bool`
|
|
84
|
+
- `do_roam_flow(roam: Roam) -> bool`
|
|
85
|
+
|
|
86
|
+
### Result 类型(返回 True/False)
|
|
87
|
+
- `do_result(ctx: Context) -> bool`
|
|
88
|
+
- `do_pack_result(pack: Pack) -> bool`
|
|
89
|
+
- `do_roam_result(roam: Roam) -> bool`
|
|
90
|
+
|
|
91
|
+
### None 类型(无返回值)
|
|
92
|
+
- `do_none(ctx: Context) -> None`
|
|
93
|
+
- `do_pack_none(pack: Pack) -> None`
|
|
94
|
+
- `do_roam_none(roam: Roam) -> None`
|
|
95
|
+
|
|
96
|
+
## 版本要求
|
|
97
|
+
|
|
98
|
+
- Python >= 3.11
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
Apache-2.0
|
|
103
|
+
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Ice Python SDK
|
|
2
|
+
|
|
3
|
+
Ice 规则引擎的 Python 实现,与 Java ice-core 和 Go SDK 功能一致。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install ice-rules
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 快速开始
|
|
12
|
+
|
|
13
|
+
### 1. 注册叶子节点
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
import ice
|
|
17
|
+
from ice import Roam
|
|
18
|
+
|
|
19
|
+
@ice.leaf("com.example.ScoreFlow")
|
|
20
|
+
class ScoreFlow:
|
|
21
|
+
threshold: int = 0
|
|
22
|
+
|
|
23
|
+
def do_roam_flow(self, roam: Roam) -> bool:
|
|
24
|
+
return roam.get_int("score", 0) >= self.threshold
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### 2. 启动客户端
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
# 同步方式
|
|
31
|
+
client = ice.FileClient(app=1, storage_path="./ice-data")
|
|
32
|
+
client.start()
|
|
33
|
+
|
|
34
|
+
pack = ice.Pack(ice_id=1)
|
|
35
|
+
pack.roam.put("score", 85)
|
|
36
|
+
results = ice.sync_process(pack)
|
|
37
|
+
|
|
38
|
+
client.destroy()
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 3. 异步方式
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
import asyncio
|
|
45
|
+
|
|
46
|
+
async def main():
|
|
47
|
+
client = ice.AsyncFileClient(app=1, storage_path="./ice-data")
|
|
48
|
+
await client.start()
|
|
49
|
+
|
|
50
|
+
pack = ice.Pack(ice_id=1)
|
|
51
|
+
pack.roam.put("score", 85)
|
|
52
|
+
results = await ice.async_process(pack)
|
|
53
|
+
|
|
54
|
+
await client.destroy()
|
|
55
|
+
|
|
56
|
+
asyncio.run(main())
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## 叶子节点类型
|
|
60
|
+
|
|
61
|
+
### Flow 类型(返回 True/False)
|
|
62
|
+
- `do_flow(ctx: Context) -> bool`
|
|
63
|
+
- `do_pack_flow(pack: Pack) -> bool`
|
|
64
|
+
- `do_roam_flow(roam: Roam) -> bool`
|
|
65
|
+
|
|
66
|
+
### Result 类型(返回 True/False)
|
|
67
|
+
- `do_result(ctx: Context) -> bool`
|
|
68
|
+
- `do_pack_result(pack: Pack) -> bool`
|
|
69
|
+
- `do_roam_result(roam: Roam) -> bool`
|
|
70
|
+
|
|
71
|
+
### None 类型(无返回值)
|
|
72
|
+
- `do_none(ctx: Context) -> None`
|
|
73
|
+
- `do_pack_none(pack: Pack) -> None`
|
|
74
|
+
- `do_roam_none(roam: Roam) -> None`
|
|
75
|
+
|
|
76
|
+
## 版本要求
|
|
77
|
+
|
|
78
|
+
- Python >= 3.11
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
Apache-2.0
|
|
83
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "setuptools_scm>=8.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ice-rules"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Ice Rule Engine Python SDK"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "Apache-2.0"}
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "waitmoon"}
|
|
14
|
+
]
|
|
15
|
+
keywords = ["ice", "rule-engine", "decision-engine"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: Apache Software License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/waitmoon/ice"
|
|
28
|
+
Documentation = "https://waitmoon.github.io/ice"
|
|
29
|
+
Repository = "https://github.com/waitmoon/ice"
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.packages.find]
|
|
32
|
+
where = ["src"]
|
|
33
|
+
|
|
34
|
+
[tool.setuptools_scm]
|
|
35
|
+
root = "../../.."
|
|
@@ -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
|
+
|
|
@@ -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
|
+
|