plaita 0.4.0__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.
Files changed (122) hide show
  1. plaita-0.4.0/MANIFEST.in +1 -0
  2. plaita-0.4.0/PKG-INFO +40 -0
  3. plaita-0.4.0/plaita/__init__.py +178 -0
  4. plaita-0.4.0/plaita/client.py +407 -0
  5. plaita-0.4.0/plaita/core/__init__.py +125 -0
  6. plaita-0.4.0/plaita/core/async_utils.py +121 -0
  7. plaita-0.4.0/plaita/core/callback.py +170 -0
  8. plaita-0.4.0/plaita/core/context.py +243 -0
  9. plaita-0.4.0/plaita/core/errors.py +119 -0
  10. plaita-0.4.0/plaita/core/executor.py +675 -0
  11. plaita-0.4.0/plaita/core/expression.py +417 -0
  12. plaita-0.4.0/plaita/core/flow.py +242 -0
  13. plaita-0.4.0/plaita/core/runner.py +240 -0
  14. plaita-0.4.0/plaita/core/types.py +118 -0
  15. plaita-0.4.0/plaita/dsl/__init__.py +94 -0
  16. plaita-0.4.0/plaita/dsl/builder.py +1053 -0
  17. plaita-0.4.0/plaita/dsl/codeflow.py +1084 -0
  18. plaita-0.4.0/plaita/dsl/sexpr.py +962 -0
  19. plaita-0.4.0/plaita/errors.py +42 -0
  20. plaita-0.4.0/plaita/event/__init__.py +107 -0
  21. plaita-0.4.0/plaita/event/core.py +360 -0
  22. plaita-0.4.0/plaita/event/demo_eventbus.py +878 -0
  23. plaita-0.4.0/plaita/event/exceptions.py +27 -0
  24. plaita-0.4.0/plaita/event/memory.py +539 -0
  25. plaita-0.4.0/plaita/event/nodes/__init__.py +0 -0
  26. plaita-0.4.0/plaita/event/redis.py +1059 -0
  27. plaita-0.4.0/plaita/event/sqlalchemy.py +858 -0
  28. plaita-0.4.0/plaita/event/timeout.py +84 -0
  29. plaita-0.4.0/plaita/event/timeout_example.py +63 -0
  30. plaita-0.4.0/plaita/event/utils.py +61 -0
  31. plaita-0.4.0/plaita/flow.py +60 -0
  32. plaita-0.4.0/plaita/io.py +800 -0
  33. plaita-0.4.0/plaita/io_format.py +110 -0
  34. plaita-0.4.0/plaita/logger.py +27 -0
  35. plaita-0.4.0/plaita/node/__init__.py +251 -0
  36. plaita-0.4.0/plaita/node/assignment.py +54 -0
  37. plaita-0.4.0/plaita/node/basic.py +69 -0
  38. plaita-0.4.0/plaita/node/calculate.py +206 -0
  39. plaita-0.4.0/plaita/node/child.py +67 -0
  40. plaita-0.4.0/plaita/node/code.py +144 -0
  41. plaita-0.4.0/plaita/node/concurrent.py +202 -0
  42. plaita-0.4.0/plaita/node/decide.py +229 -0
  43. plaita-0.4.0/plaita/node/end.py +41 -0
  44. plaita-0.4.0/plaita/node/event_node.py +255 -0
  45. plaita-0.4.0/plaita/node/http.py +438 -0
  46. plaita-0.4.0/plaita/node/loop.py +190 -0
  47. plaita-0.4.0/plaita/node/redis.py +41 -0
  48. plaita-0.4.0/plaita/node/start.py +11 -0
  49. plaita-0.4.0/plaita/server/__init__.py +29 -0
  50. plaita-0.4.0/plaita/server/control.py +268 -0
  51. plaita-0.4.0/plaita/server/event_filter.py +291 -0
  52. plaita-0.4.0/plaita/server/extended_nodes_demo.py +956 -0
  53. plaita-0.4.0/plaita/server/factory.py +101 -0
  54. plaita-0.4.0/plaita/server/flow_worker.py +637 -0
  55. plaita-0.4.0/plaita/server/log_handler.py +284 -0
  56. plaita-0.4.0/plaita/server/nodes/__init__.py +22 -0
  57. plaita-0.4.0/plaita/server/nodes/approval_node.py +233 -0
  58. plaita-0.4.0/plaita/server/nodes/base_extended_node.py +70 -0
  59. plaita-0.4.0/plaita/server/nodes/delay_node.py +127 -0
  60. plaita-0.4.0/plaita/server/nodes/http_callback_node.py +249 -0
  61. plaita-0.4.0/plaita/server/nodes/kafka_queue_node.py +201 -0
  62. plaita-0.4.0/plaita/server/nodes/redis_queue_node.py +172 -0
  63. plaita-0.4.0/plaita/server/registry.py +507 -0
  64. plaita-0.4.0/plaita/server/services/__init__.py +22 -0
  65. plaita-0.4.0/plaita/server/services/__main__.py +130 -0
  66. plaita-0.4.0/plaita/server/services/approval_service.py +230 -0
  67. plaita-0.4.0/plaita/server/services/base_service.py +398 -0
  68. plaita-0.4.0/plaita/server/services/delay_service.py +184 -0
  69. plaita-0.4.0/plaita/server/services/http_callback_service.py +139 -0
  70. plaita-0.4.0/plaita/server/services/kafka_queue_service.py +804 -0
  71. plaita-0.4.0/plaita/server/services/redis_queue_service.py +659 -0
  72. plaita-0.4.0/plaita/server/services/service_manager.py +269 -0
  73. plaita-0.4.0/plaita/storage/__init__.py +31 -0
  74. plaita-0.4.0/plaita/storage/base.py +157 -0
  75. plaita-0.4.0/plaita/storage/memory.py +142 -0
  76. plaita-0.4.0/plaita/storage/redis.py +405 -0
  77. plaita-0.4.0/plaita/storage/sqlalchemy.py +454 -0
  78. plaita-0.4.0/plaita/types.py +59 -0
  79. plaita-0.4.0/plaita.egg-info/PKG-INFO +40 -0
  80. plaita-0.4.0/plaita.egg-info/SOURCES.txt +120 -0
  81. plaita-0.4.0/plaita.egg-info/dependency_links.txt +1 -0
  82. plaita-0.4.0/plaita.egg-info/entry_points.txt +6 -0
  83. plaita-0.4.0/plaita.egg-info/requires.txt +39 -0
  84. plaita-0.4.0/plaita.egg-info/top_level.txt +1 -0
  85. plaita-0.4.0/pyproject.toml +88 -0
  86. plaita-0.4.0/requirements.txt +28 -0
  87. plaita-0.4.0/setup.cfg +4 -0
  88. plaita-0.4.0/setup.py +8 -0
  89. plaita-0.4.0/tests/test_approval_integration.py +710 -0
  90. plaita-0.4.0/tests/test_async_flow.py +119 -0
  91. plaita-0.4.0/tests/test_calculate.py +72 -0
  92. plaita-0.4.0/tests/test_checkpoint_resume.py +338 -0
  93. plaita-0.4.0/tests/test_client_default_url.py +19 -0
  94. plaita-0.4.0/tests/test_code.py +120 -0
  95. plaita-0.4.0/tests/test_concurrent.py +361 -0
  96. plaita-0.4.0/tests/test_control.py +162 -0
  97. plaita-0.4.0/tests/test_decide.py +336 -0
  98. plaita-0.4.0/tests/test_delay_integration.py +513 -0
  99. plaita-0.4.0/tests/test_errors.py +74 -0
  100. plaita-0.4.0/tests/test_evaluate.py +126 -0
  101. plaita-0.4.0/tests/test_event_filter_dedup.py +250 -0
  102. plaita-0.4.0/tests/test_event_system.py +338 -0
  103. plaita-0.4.0/tests/test_extended_nodes.py +699 -0
  104. plaita-0.4.0/tests/test_flow.py +540 -0
  105. plaita-0.4.0/tests/test_flow_distributed.py +176 -0
  106. plaita-0.4.0/tests/test_flow_worker.py +265 -0
  107. plaita-0.4.0/tests/test_flow_worker_callbacks.py +79 -0
  108. plaita-0.4.0/tests/test_flow_worker_scenarios.py +513 -0
  109. plaita-0.4.0/tests/test_from_file.py +18 -0
  110. plaita-0.4.0/tests/test_http.py +692 -0
  111. plaita-0.4.0/tests/test_inline.py +73 -0
  112. plaita-0.4.0/tests/test_io.py +358 -0
  113. plaita-0.4.0/tests/test_log_handler.py +193 -0
  114. plaita-0.4.0/tests/test_loop.py +464 -0
  115. plaita-0.4.0/tests/test_performance_benchmark.py +584 -0
  116. plaita-0.4.0/tests/test_redis.py +105 -0
  117. plaita-0.4.0/tests/test_registry.py +328 -0
  118. plaita-0.4.0/tests/test_storage.py +173 -0
  119. plaita-0.4.0/tests/test_storage_commons.py +163 -0
  120. plaita-0.4.0/tests/test_storage_main.py +59 -0
  121. plaita-0.4.0/tests/test_storage_redis.py +225 -0
  122. plaita-0.4.0/tests/test_types.py +58 -0
@@ -0,0 +1 @@
1
+ include requirements.txt
plaita-0.4.0/PKG-INFO ADDED
@@ -0,0 +1,40 @@
1
+ Metadata-Version: 2.4
2
+ Name: plaita
3
+ Version: 0.4.0
4
+ Summary: Official Python Runtime for Plaita -- Logic Compose Engine
5
+ Author-email: Jeff kit <bbmyth@gmail.com>
6
+ Project-URL: Homepage, https://jeffkit.github.io/plaita/
7
+ Project-URL: Repository, https://github.com/jeffkit/plaita
8
+ Project-URL: Documentation, https://jeffkit.github.io/plaita/
9
+ Requires-Python: >=3.10
10
+ Requires-Dist: pydantic>=2.0
11
+ Requires-Dist: pyparsing>=3.0
12
+ Requires-Dist: isodate
13
+ Provides-Extra: redis
14
+ Requires-Dist: redis>=4.0.0; extra == "redis"
15
+ Provides-Extra: server
16
+ Requires-Dist: fastapi>=0.68.0; extra == "server"
17
+ Requires-Dist: uvicorn>=0.15.0; extra == "server"
18
+ Requires-Dist: python-multipart>=0.0.5; extra == "server"
19
+ Requires-Dist: python-jose[cryptography]>=3.3.0; extra == "server"
20
+ Requires-Dist: passlib[bcrypt]>=1.7.4; extra == "server"
21
+ Requires-Dist: SQLAlchemy>=1.4.0; extra == "server"
22
+ Requires-Dist: cachetools>=5.0.0; extra == "server"
23
+ Provides-Extra: code
24
+ Requires-Dist: PyExecJS; extra == "code"
25
+ Provides-Extra: http
26
+ Requires-Dist: requests; extra == "http"
27
+ Requires-Dist: aiohttp>=3.8.0; extra == "http"
28
+ Provides-Extra: yaml
29
+ Requires-Dist: PyYAML>=6.0; extra == "yaml"
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest; extra == "dev"
32
+ Requires-Dist: pytest-asyncio; extra == "dev"
33
+ Requires-Dist: fakeredis; extra == "dev"
34
+ Requires-Dist: pytest-cov; extra == "dev"
35
+ Provides-Extra: lint
36
+ Requires-Dist: mypy; extra == "lint"
37
+ Requires-Dist: flake8; extra == "lint"
38
+ Requires-Dist: black; extra == "lint"
39
+ Provides-Extra: all
40
+ Requires-Dist: plaita[code,http,redis,server,yaml]; extra == "all"
@@ -0,0 +1,178 @@
1
+ __version__ = "0.3.16"
2
+
3
+ import logging as _logging
4
+
5
+ _logger = _logging.getLogger(__name__)
6
+
7
+ _EXTRAS_GUIDE = {
8
+ "redis": ("redis", "pip install plaita[redis]"),
9
+ "server": ("fastapi", "pip install plaita[server]"),
10
+ "code": ("execjs", "pip install plaita[code]"),
11
+ "http": ("requests", "pip install plaita[http]"),
12
+ }
13
+
14
+
15
+ def _emit_extras_guidance():
16
+ """Log guidance once when optional extras are missing, at DEBUG level."""
17
+ missing = []
18
+ for extra, (probe_module, install_cmd) in _EXTRAS_GUIDE.items():
19
+ try:
20
+ __import__(probe_module)
21
+ except ImportError:
22
+ missing.append((extra, install_cmd))
23
+ if missing:
24
+ names = ", ".join(name for name, _ in missing)
25
+ _logger.debug(
26
+ "Optional plaita extras not installed: %s. "
27
+ "Install what you need, e.g.: pip install plaita[all]",
28
+ names,
29
+ )
30
+
31
+
32
+ _emit_extras_guidance()
33
+
34
+
35
+ def _check_extra_available(extra_name: str) -> bool:
36
+ """Check if a specific extra dependency group is available.
37
+
38
+ Returns True if the probe module for the extra can be imported.
39
+ Raises ImportError with an actionable message if not.
40
+ """
41
+ if extra_name not in _EXTRAS_GUIDE:
42
+ return True
43
+ probe_module, install_cmd = _EXTRAS_GUIDE[extra_name]
44
+ try:
45
+ __import__(probe_module)
46
+ return True
47
+ except ImportError:
48
+ raise ImportError(
49
+ f"The '{extra_name}' extra is required for this feature but is not installed. "
50
+ f"Install it with: {install_cmd}\n"
51
+ f"Or install all extras: pip install plaita[all]"
52
+ )
53
+
54
+
55
+ # 依赖反转: 注册一个 lazy 的默认 event bus provider, 让 plaita.core.context
56
+ # 在需要时自动取默认总线, 而 core 层不必 import plaita.event (避免 core→event
57
+ # 反向依赖)。provider 仅在第一次被调用时才 import plaita.event。
58
+ def _default_event_bus_provider():
59
+ from plaita.event import get_default_event_bus
60
+ return get_default_event_bus()
61
+
62
+
63
+ def _register_default_event_bus_provider() -> None:
64
+ try:
65
+ from plaita.core.context import set_default_event_bus_provider
66
+ set_default_event_bus_provider(_default_event_bus_provider)
67
+ except Exception:
68
+ _logger.debug("Unable to register default event bus provider", exc_info=True)
69
+
70
+
71
+ _register_default_event_bus_provider()
72
+
73
+
74
+ # Upgrade guide: provide clear error messages when accessing optional features
75
+ # without the corresponding extras installed. Consumed by __getattr__ below.
76
+ _FEATURE_EXTRAS_MAP = {
77
+ "FlowWorker": "server",
78
+ "ManagementAPI": "server",
79
+ "RedisStorage": "redis",
80
+ "RedisEventBus": "redis",
81
+ "CodeNode": "code",
82
+ "HTTP": "http",
83
+ }
84
+
85
+ # Canonical home for each lazily re-exported public name. Kept explicit so
86
+ # that `from plaita import Flow` works out of the box without eagerly pulling
87
+ # the core layer into memory at package import time.
88
+ _LAZY_EXPORTS = {
89
+ # plaita.core.flow
90
+ "Flow": "plaita.core.flow",
91
+ "parse": "plaita.core.flow",
92
+ "parse_and_run": "plaita.core.flow",
93
+ # plaita.core.executor
94
+ "FlowExecution": "plaita.core.executor",
95
+ "ExecutionMode": "plaita.core.executor",
96
+ # plaita.core.errors
97
+ "FlowExecutionException": "plaita.core.errors",
98
+ "FlowErrorType": "plaita.core.errors",
99
+ "FlowResultError": "plaita.core.errors",
100
+ "NodeException": "plaita.core.errors",
101
+ "ErrorStrategy": "plaita.core.errors",
102
+ "ErrorHandler": "plaita.core.errors",
103
+ "RecoverableErrorHandler": "plaita.core.errors",
104
+ # plaita.core.callback
105
+ "FlowCallback": "plaita.core.callback",
106
+ "FlowEvent": "plaita.core.callback",
107
+ "CallbackManager": "plaita.core.callback",
108
+ "BaseCallbackManager": "plaita.core.callback",
109
+ "LoggerCallback": "plaita.core.callback",
110
+ # plaita.node
111
+ "Node": "plaita.node",
112
+ "node_register": "plaita.node",
113
+ "parse_node": "plaita.node",
114
+ "nodes": "plaita.node",
115
+ "NodeRegistry": "plaita.node",
116
+ "get_default_registry": "plaita.node",
117
+ }
118
+
119
+ # For optional-feature names: once the required extra is confirmed available,
120
+ # the name lives in this canonical module. ``HTTP`` (not ``HTTPNode``) is the
121
+ # real class name exported by ``plaita.node.http``.
122
+ _EXTRA_EXPORTS = {
123
+ "FlowWorker": "plaita.server.flow_worker",
124
+ "ManagementAPI": "plaita.server.control",
125
+ "RedisStorage": "plaita.storage.redis",
126
+ "RedisEventBus": "plaita.event.redis",
127
+ "CodeNode": "plaita.node.code",
128
+ "HTTP": "plaita.node.http",
129
+ }
130
+
131
+
132
+ def __getattr__(name: str):
133
+ """Lazy top-level re-exports + actionable extras guidance.
134
+
135
+ - ``from plaita import Flow`` / ``parse`` / ``Node`` / ``node_register`` ...
136
+ resolve to the canonical ``plaita.core.*`` / ``plaita.node`` symbols without
137
+ importing the core layer at package import time.
138
+ - ``from plaita import types`` returns the ``plaita.core.types`` module so the
139
+ deprecated ``plaita.types`` shim is NOT triggered.
140
+ - Optional features (``FlowWorker``, ``HTTP``, ``RedisEventBus`` ...) raise
141
+ a clear ImportError naming the missing extra instead of a bare ImportError.
142
+ """
143
+ import importlib
144
+
145
+ if name in _LAZY_EXPORTS:
146
+ module = importlib.import_module(_LAZY_EXPORTS[name])
147
+ value = getattr(module, name)
148
+ globals()[name] = value # cache so subsequent access bypasses __getattr__
149
+ return value
150
+
151
+ if name == "types":
152
+ module = importlib.import_module("plaita.core.types")
153
+ globals()["types"] = module
154
+ return module
155
+
156
+ if name in _FEATURE_EXTRAS_MAP:
157
+ # Raises an actionable ImportError if the extra is missing.
158
+ _check_extra_available(_FEATURE_EXTRAS_MAP[name])
159
+ module = importlib.import_module(_EXTRA_EXPORTS[name])
160
+ value = getattr(module, name)
161
+ globals()[name] = value
162
+ return value
163
+
164
+ raise AttributeError(f"module 'plaita' has no attribute {name!r}")
165
+
166
+
167
+ def __dir__():
168
+ """Make lazy re-exports discoverable by IDEs / ``dir(plaita)``."""
169
+ return sorted(
170
+ set(globals())
171
+ | set(_LAZY_EXPORTS)
172
+ | set(_FEATURE_EXTRAS_MAP)
173
+ | {"types"}
174
+ )
175
+
176
+
177
+ if __name__ == "__main__":
178
+ print(__version__)
@@ -0,0 +1,407 @@
1
+ import hashlib
2
+ import hmac
3
+ import json
4
+ import logging
5
+ from threading import Lock
6
+ from time import time
7
+ from typing import Any, Dict, Optional, Union
8
+ from urllib.parse import urlencode
9
+
10
+ import requests
11
+
12
+ from plaita.flow import Flow
13
+
14
+ # 获取logger
15
+ logger = logging.getLogger("plaita.client")
16
+
17
+ # Constants
18
+ DEFAULT_SIGNATURE_EXPIRATION = 3 # 3 seconds
19
+ DEFAULT_REDIS_TTL = 3600 # 1 hour cache TTL in Redis
20
+ # 默认指向本仓库 plaita-console 控制台提供的流程定义契约接口
21
+ # (POST /api/flowVersion/semver/detail,HMAC 鉴权)。可通过 url 参数覆盖。
22
+ DEFAULT_CONSOLE_URL = "http://localhost:8080/api/flowVersion/semver/detail"
23
+
24
+
25
+ def _get_config_key(flow_id, version):
26
+ return f"flow:{flow_id}:{version}"
27
+
28
+
29
+ class RedisConfig:
30
+ """Redis 配置类"""
31
+
32
+ def __init__(
33
+ self,
34
+ host: str = "localhost",
35
+ port: int = 6379,
36
+ db: int = 0,
37
+ password: Optional[str] = None,
38
+ socket_timeout: float = 5.0,
39
+ socket_connect_timeout: float = 5.0,
40
+ decode_responses: bool = True,
41
+ **kwargs
42
+ ):
43
+ self.host = host
44
+ self.port = port
45
+ self.db = db
46
+ self.password = password
47
+ self.socket_timeout = socket_timeout
48
+ self.socket_connect_timeout = socket_connect_timeout
49
+ self.decode_responses = decode_responses
50
+ self.extra_options = kwargs
51
+
52
+ def to_dict(self) -> Dict[str, Any]:
53
+ """转换为字典格式"""
54
+ config = {
55
+ "host": self.host,
56
+ "port": self.port,
57
+ "db": self.db,
58
+ "socket_timeout": self.socket_timeout,
59
+ "socket_connect_timeout": self.socket_connect_timeout,
60
+ "decode_responses": self.decode_responses,
61
+ }
62
+ if self.password:
63
+ config["password"] = self.password
64
+ config.update(self.extra_options)
65
+ return config
66
+
67
+
68
+ class PlaitaClient:
69
+ """
70
+ Plaita 客户端,用于从远程服务获取流程定义并执行
71
+
72
+ 支持多级缓存:
73
+ 1. 内存缓存(最快)
74
+ 2. Redis缓存(可选,跨进程共享)
75
+ 3. 远程服务(最新数据)
76
+
77
+ 使用示例:
78
+ # 基础用法
79
+ client = PlaitaClient(secret_id='xxx', secret_key='yyy', url='https://your-plaita-server/api/flowVersion/semver/detail')
80
+ result = client.run_flow('flow_id', '1.0.0', {"param": "value"})
81
+
82
+ # 使用 Redis 缓存
83
+ client = PlaitaClient(
84
+ secret_id='xxx',
85
+ secret_key='yyy',
86
+ url='https://your-plaita-server/api/flowVersion/semver/detail',
87
+ redis_config=RedisConfig(host='localhost', port=6379)
88
+ )
89
+
90
+ # 使用已有的 Redis 客户端
91
+ import redis
92
+ redis_client = redis.Redis(host='localhost', port=6379, decode_responses=True)
93
+ client = PlaitaClient(
94
+ secret_id='xxx',
95
+ secret_key='yyy',
96
+ url='https://your-plaita-server/api/flowVersion/semver/detail',
97
+ redis_client=redis_client
98
+ )
99
+ """
100
+
101
+ def __init__(
102
+ self,
103
+ secret_id: str,
104
+ secret_key: str,
105
+ url: str = DEFAULT_CONSOLE_URL,
106
+ signature_validity: int = DEFAULT_SIGNATURE_EXPIRATION,
107
+ headers: Optional[Dict[str, str]] = None,
108
+ redis_client: Optional[Any] = None,
109
+ redis_config: Optional[Union[RedisConfig, Dict[str, Any]]] = None,
110
+ redis_ttl: int = DEFAULT_REDIS_TTL,
111
+ ):
112
+ """
113
+ 初始化 PlaitaClient
114
+
115
+ Args:
116
+ secret_id: API 密钥 ID
117
+ secret_key: API 密钥
118
+ url: API 服务地址,默认指向本仓库 plaita-console 控制台的
119
+ ``/api/flowVersion/semver/detail`` 契约接口(本地部署)。
120
+ 生产环境请通过该参数指向你部署的控制台地址。
121
+ signature_validity: 签名有效期(秒)
122
+ headers: 额外的请求头
123
+ redis_client: 已有的 Redis 客户端实例(优先使用)
124
+ redis_config: Redis 配置(当 redis_client 为 None 时使用)
125
+ redis_ttl: Redis 缓存过期时间(秒)
126
+ """
127
+ self.secret_id = secret_id
128
+ self.secret_key = secret_key
129
+ self.signature_validity = signature_validity
130
+ self.url = url
131
+ self.headers = headers if headers is not None else {}
132
+ self.redis_ttl = redis_ttl
133
+ self.memory_cache = {}
134
+ self.memory_cache_lock = Lock()
135
+
136
+ # 初始化 Redis 客户端
137
+ self._redis_client = None
138
+ self._redis_available = False
139
+
140
+ if redis_client is not None:
141
+ # 使用传入的 Redis 客户端
142
+ self._redis_client = redis_client
143
+ self._validate_redis_connection()
144
+ elif redis_config is not None:
145
+ # 根据配置创建 Redis 客户端
146
+ self._init_redis_from_config(redis_config)
147
+
148
+ def _init_redis_from_config(self, config: Union[RedisConfig, Dict[str, Any]]) -> None:
149
+ """
150
+ 根据配置初始化 Redis 客户端
151
+
152
+ Args:
153
+ config: Redis 配置对象或字典
154
+ """
155
+ try:
156
+ import redis
157
+ except ImportError:
158
+ logger.warning("Redis 库未安装,Redis 缓存将不可用。请运行: pip install redis")
159
+ return
160
+
161
+ try:
162
+ if isinstance(config, dict):
163
+ config = RedisConfig(**config)
164
+
165
+ self._redis_client = redis.Redis(**config.to_dict())
166
+ self._validate_redis_connection()
167
+ except Exception as e:
168
+ logger.warning(f"初始化 Redis 客户端失败: {e}")
169
+ self._redis_client = None
170
+ self._redis_available = False
171
+
172
+ def _validate_redis_connection(self) -> bool:
173
+ """
174
+ 验证 Redis 连接是否可用
175
+
176
+ Returns:
177
+ bool: 连接是否可用
178
+ """
179
+ if self._redis_client is None:
180
+ self._redis_available = False
181
+ return False
182
+
183
+ try:
184
+ self._redis_client.ping()
185
+ self._redis_available = True
186
+ logger.info("Redis 连接验证成功")
187
+ return True
188
+ except Exception as e:
189
+ logger.warning(f"Redis 连接验证失败: {e}")
190
+ self._redis_available = False
191
+ return False
192
+
193
+ @property
194
+ def redis_client(self) -> Optional[Any]:
195
+ """获取 Redis 客户端(兼容旧代码)"""
196
+ return self._redis_client if self._redis_available else None
197
+
198
+ def run_flow(self, flow_id: str, version: str, input_data: Optional[Dict] = None):
199
+ """
200
+ 获取并执行流程
201
+
202
+ Args:
203
+ flow_id: 流程 ID
204
+ version: 流程版本
205
+ input_data: 输入参数
206
+
207
+ Returns:
208
+ 流程执行结果
209
+ """
210
+ flow = self.get_flow(flow_id, version)
211
+ return flow.run(input_data)
212
+
213
+ def get_flow(self, flow_id: str, version: str) -> Flow:
214
+ """
215
+ 获取流程定义
216
+
217
+ 优先级:内存缓存 > Redis缓存 > 远程服务
218
+
219
+ Args:
220
+ flow_id: 流程 ID
221
+ version: 流程版本
222
+
223
+ Returns:
224
+ Flow: 流程对象
225
+ """
226
+ cache_key = _get_config_key(flow_id, version)
227
+
228
+ # 1. 优先从内存缓存获取
229
+ with self.memory_cache_lock:
230
+ cached_flow = self.memory_cache.get(cache_key)
231
+ if cached_flow:
232
+ logger.debug(f"从内存缓存获取流程: {cache_key}")
233
+ return cached_flow
234
+
235
+ # 2. 尝试从 Redis 缓存获取
236
+ if self._redis_available and self._redis_client:
237
+ try:
238
+ config_data = self._redis_client.get(cache_key)
239
+ if config_data:
240
+ logger.debug(f"从 Redis 缓存获取流程: {cache_key}")
241
+ flow_obj = Flow.model_validate_json(json.loads(config_data))
242
+ # 更新内存缓存
243
+ with self.memory_cache_lock:
244
+ self.memory_cache[cache_key] = flow_obj
245
+ return flow_obj
246
+ except Exception as e:
247
+ logger.warning(f"从 Redis 获取缓存失败: {e}")
248
+
249
+ # 3. 从远程服务获取
250
+ logger.debug(f"从远程服务获取流程: {cache_key}")
251
+ flow_data = self._fetch_flow(flow_id, version)
252
+ flow_obj = Flow.model_validate_json(flow_data)
253
+
254
+ # 更新缓存
255
+ self._update_cache(cache_key, flow_obj, flow_data)
256
+
257
+ return flow_obj
258
+
259
+ def _update_cache(self, cache_key: str, flow_obj: Flow, flow_data: Any) -> None:
260
+ """
261
+ 更新缓存
262
+
263
+ Args:
264
+ cache_key: 缓存键
265
+ flow_obj: 流程对象
266
+ flow_data: 原始流程数据
267
+ """
268
+ # 更新内存缓存
269
+ with self.memory_cache_lock:
270
+ self.memory_cache[cache_key] = flow_obj
271
+
272
+ # 更新 Redis 缓存
273
+ if self._redis_available and self._redis_client:
274
+ try:
275
+ self._redis_client.set(
276
+ cache_key,
277
+ json.dumps(flow_data),
278
+ ex=self.redis_ttl # 设置过期时间
279
+ )
280
+ logger.debug(f"已更新 Redis 缓存: {cache_key}, TTL: {self.redis_ttl}s")
281
+ except Exception as e:
282
+ logger.warning(f"更新 Redis 缓存失败: {e}")
283
+
284
+ def clear_cache(self, flow_id: Optional[str] = None, version: Optional[str] = None) -> int:
285
+ """
286
+ 清除缓存
287
+
288
+ Args:
289
+ flow_id: 流程 ID(可选,不指定则清除所有)
290
+ version: 流程版本(可选)
291
+
292
+ Returns:
293
+ int: 清除的缓存项数量
294
+ """
295
+ cleared_count = 0
296
+
297
+ if flow_id and version:
298
+ # 清除特定版本的缓存
299
+ cache_key = _get_config_key(flow_id, version)
300
+ with self.memory_cache_lock:
301
+ if cache_key in self.memory_cache:
302
+ del self.memory_cache[cache_key]
303
+ cleared_count += 1
304
+
305
+ if self._redis_available and self._redis_client:
306
+ try:
307
+ self._redis_client.delete(cache_key)
308
+ except Exception as e:
309
+ logger.warning(f"清除 Redis 缓存失败: {e}")
310
+ else:
311
+ # 清除所有缓存
312
+ with self.memory_cache_lock:
313
+ cleared_count = len(self.memory_cache)
314
+ self.memory_cache.clear()
315
+
316
+ logger.info(f"已清除 {cleared_count} 个内存缓存项")
317
+
318
+ return cleared_count
319
+
320
+ def _fetch_flow(self, flow_id: str, version: str) -> Any:
321
+ """
322
+ 从远程服务获取流程定义
323
+
324
+ Args:
325
+ flow_id: 流程 ID
326
+ version: 流程版本
327
+
328
+ Returns:
329
+ 解析后的流程定义数据
330
+
331
+ Raises:
332
+ Exception: 获取流程失败时抛出异常
333
+ """
334
+ data = {"flowId": flow_id, "version": version}
335
+ headers = {
336
+ "Content-Type": "application/x-www-form-urlencoded",
337
+ "Authorization": generate_signature(self.secret_key, self.secret_id, self.signature_validity, int(time())),
338
+ }
339
+ headers.update(self.headers) # 合并额外的请求头
340
+
341
+ try:
342
+ response = requests.post(self.url, headers=headers, data=data, timeout=30)
343
+ except requests.exceptions.Timeout:
344
+ raise Exception(f"请求超时: flow_id={flow_id}, version={version}")
345
+ except requests.exceptions.RequestException as e:
346
+ raise Exception(f"请求失败: {e}")
347
+
348
+ if response.status_code != 200:
349
+ raise Exception(f"获取流程配置失败, HTTP状态码: {response.status_code}")
350
+
351
+ try:
352
+ response_data = response.json()
353
+ except json.JSONDecodeError:
354
+ raise Exception("响应数据不是有效的 JSON 格式")
355
+
356
+ if response_data.get("code"):
357
+ raise Exception(f"获取流程配置失败: {response_data.get('message', '未知错误')}")
358
+
359
+ if response_data.get("data") is None:
360
+ raise Exception("获取流程配置失败: 响应数据为空")
361
+
362
+ flow_str = response_data["data"].get("flow")
363
+ if not flow_str:
364
+ raise Exception("获取流程配置失败: flow 字段为空")
365
+
366
+ logger.debug(f"成功获取流程定义: flow_id={flow_id}, version={version}")
367
+ return json.loads(flow_str)
368
+
369
+
370
+ def generate_signature(secret_key: str, secret_id: str, signature_validity: int, sign_time: int) -> str:
371
+ """
372
+ 生成 API 请求签名
373
+
374
+ Args:
375
+ secret_key: API 密钥
376
+ secret_id: API 密钥 ID
377
+ signature_validity: 签名有效期(秒)
378
+ sign_time: 签名时间戳
379
+
380
+ Returns:
381
+ str: URL 编码的签名字符串
382
+ """
383
+ signature_validity = max(DEFAULT_SIGNATURE_EXPIRATION, signature_validity)
384
+
385
+ sign_expire = sign_time + signature_validity
386
+ key_time = f"{sign_time};{sign_expire}"
387
+
388
+ key = hmac.new(secret_key.encode(), key_time.encode(), hashlib.sha256)
389
+ key_string = key.hexdigest()
390
+
391
+ string_to_sign = f"{sign_time}\n"
392
+ signature_key = hmac.new(key_string.encode(), string_to_sign.encode(), hashlib.sha256)
393
+ sign = signature_key.hexdigest()
394
+
395
+ data = urlencode({"secret-id": secret_id, "sign-time": str(sign_time), "key-time": key_time, "signature": sign})
396
+ return data
397
+
398
+
399
+ # Example Usage
400
+ #
401
+ # client = PlaitaClient(
402
+ # 'your secret id',
403
+ # 'your secret key',
404
+ # url='https://your-plaita-server/api/flowVersion/semver/detail',
405
+ # )
406
+ # result = client.run_flow('259', '0.0.2', {"age": 14})
407
+ # print(result)