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.
- plaita-0.4.0/MANIFEST.in +1 -0
- plaita-0.4.0/PKG-INFO +40 -0
- plaita-0.4.0/plaita/__init__.py +178 -0
- plaita-0.4.0/plaita/client.py +407 -0
- plaita-0.4.0/plaita/core/__init__.py +125 -0
- plaita-0.4.0/plaita/core/async_utils.py +121 -0
- plaita-0.4.0/plaita/core/callback.py +170 -0
- plaita-0.4.0/plaita/core/context.py +243 -0
- plaita-0.4.0/plaita/core/errors.py +119 -0
- plaita-0.4.0/plaita/core/executor.py +675 -0
- plaita-0.4.0/plaita/core/expression.py +417 -0
- plaita-0.4.0/plaita/core/flow.py +242 -0
- plaita-0.4.0/plaita/core/runner.py +240 -0
- plaita-0.4.0/plaita/core/types.py +118 -0
- plaita-0.4.0/plaita/dsl/__init__.py +94 -0
- plaita-0.4.0/plaita/dsl/builder.py +1053 -0
- plaita-0.4.0/plaita/dsl/codeflow.py +1084 -0
- plaita-0.4.0/plaita/dsl/sexpr.py +962 -0
- plaita-0.4.0/plaita/errors.py +42 -0
- plaita-0.4.0/plaita/event/__init__.py +107 -0
- plaita-0.4.0/plaita/event/core.py +360 -0
- plaita-0.4.0/plaita/event/demo_eventbus.py +878 -0
- plaita-0.4.0/plaita/event/exceptions.py +27 -0
- plaita-0.4.0/plaita/event/memory.py +539 -0
- plaita-0.4.0/plaita/event/nodes/__init__.py +0 -0
- plaita-0.4.0/plaita/event/redis.py +1059 -0
- plaita-0.4.0/plaita/event/sqlalchemy.py +858 -0
- plaita-0.4.0/plaita/event/timeout.py +84 -0
- plaita-0.4.0/plaita/event/timeout_example.py +63 -0
- plaita-0.4.0/plaita/event/utils.py +61 -0
- plaita-0.4.0/plaita/flow.py +60 -0
- plaita-0.4.0/plaita/io.py +800 -0
- plaita-0.4.0/plaita/io_format.py +110 -0
- plaita-0.4.0/plaita/logger.py +27 -0
- plaita-0.4.0/plaita/node/__init__.py +251 -0
- plaita-0.4.0/plaita/node/assignment.py +54 -0
- plaita-0.4.0/plaita/node/basic.py +69 -0
- plaita-0.4.0/plaita/node/calculate.py +206 -0
- plaita-0.4.0/plaita/node/child.py +67 -0
- plaita-0.4.0/plaita/node/code.py +144 -0
- plaita-0.4.0/plaita/node/concurrent.py +202 -0
- plaita-0.4.0/plaita/node/decide.py +229 -0
- plaita-0.4.0/plaita/node/end.py +41 -0
- plaita-0.4.0/plaita/node/event_node.py +255 -0
- plaita-0.4.0/plaita/node/http.py +438 -0
- plaita-0.4.0/plaita/node/loop.py +190 -0
- plaita-0.4.0/plaita/node/redis.py +41 -0
- plaita-0.4.0/plaita/node/start.py +11 -0
- plaita-0.4.0/plaita/server/__init__.py +29 -0
- plaita-0.4.0/plaita/server/control.py +268 -0
- plaita-0.4.0/plaita/server/event_filter.py +291 -0
- plaita-0.4.0/plaita/server/extended_nodes_demo.py +956 -0
- plaita-0.4.0/plaita/server/factory.py +101 -0
- plaita-0.4.0/plaita/server/flow_worker.py +637 -0
- plaita-0.4.0/plaita/server/log_handler.py +284 -0
- plaita-0.4.0/plaita/server/nodes/__init__.py +22 -0
- plaita-0.4.0/plaita/server/nodes/approval_node.py +233 -0
- plaita-0.4.0/plaita/server/nodes/base_extended_node.py +70 -0
- plaita-0.4.0/plaita/server/nodes/delay_node.py +127 -0
- plaita-0.4.0/plaita/server/nodes/http_callback_node.py +249 -0
- plaita-0.4.0/plaita/server/nodes/kafka_queue_node.py +201 -0
- plaita-0.4.0/plaita/server/nodes/redis_queue_node.py +172 -0
- plaita-0.4.0/plaita/server/registry.py +507 -0
- plaita-0.4.0/plaita/server/services/__init__.py +22 -0
- plaita-0.4.0/plaita/server/services/__main__.py +130 -0
- plaita-0.4.0/plaita/server/services/approval_service.py +230 -0
- plaita-0.4.0/plaita/server/services/base_service.py +398 -0
- plaita-0.4.0/plaita/server/services/delay_service.py +184 -0
- plaita-0.4.0/plaita/server/services/http_callback_service.py +139 -0
- plaita-0.4.0/plaita/server/services/kafka_queue_service.py +804 -0
- plaita-0.4.0/plaita/server/services/redis_queue_service.py +659 -0
- plaita-0.4.0/plaita/server/services/service_manager.py +269 -0
- plaita-0.4.0/plaita/storage/__init__.py +31 -0
- plaita-0.4.0/plaita/storage/base.py +157 -0
- plaita-0.4.0/plaita/storage/memory.py +142 -0
- plaita-0.4.0/plaita/storage/redis.py +405 -0
- plaita-0.4.0/plaita/storage/sqlalchemy.py +454 -0
- plaita-0.4.0/plaita/types.py +59 -0
- plaita-0.4.0/plaita.egg-info/PKG-INFO +40 -0
- plaita-0.4.0/plaita.egg-info/SOURCES.txt +120 -0
- plaita-0.4.0/plaita.egg-info/dependency_links.txt +1 -0
- plaita-0.4.0/plaita.egg-info/entry_points.txt +6 -0
- plaita-0.4.0/plaita.egg-info/requires.txt +39 -0
- plaita-0.4.0/plaita.egg-info/top_level.txt +1 -0
- plaita-0.4.0/pyproject.toml +88 -0
- plaita-0.4.0/requirements.txt +28 -0
- plaita-0.4.0/setup.cfg +4 -0
- plaita-0.4.0/setup.py +8 -0
- plaita-0.4.0/tests/test_approval_integration.py +710 -0
- plaita-0.4.0/tests/test_async_flow.py +119 -0
- plaita-0.4.0/tests/test_calculate.py +72 -0
- plaita-0.4.0/tests/test_checkpoint_resume.py +338 -0
- plaita-0.4.0/tests/test_client_default_url.py +19 -0
- plaita-0.4.0/tests/test_code.py +120 -0
- plaita-0.4.0/tests/test_concurrent.py +361 -0
- plaita-0.4.0/tests/test_control.py +162 -0
- plaita-0.4.0/tests/test_decide.py +336 -0
- plaita-0.4.0/tests/test_delay_integration.py +513 -0
- plaita-0.4.0/tests/test_errors.py +74 -0
- plaita-0.4.0/tests/test_evaluate.py +126 -0
- plaita-0.4.0/tests/test_event_filter_dedup.py +250 -0
- plaita-0.4.0/tests/test_event_system.py +338 -0
- plaita-0.4.0/tests/test_extended_nodes.py +699 -0
- plaita-0.4.0/tests/test_flow.py +540 -0
- plaita-0.4.0/tests/test_flow_distributed.py +176 -0
- plaita-0.4.0/tests/test_flow_worker.py +265 -0
- plaita-0.4.0/tests/test_flow_worker_callbacks.py +79 -0
- plaita-0.4.0/tests/test_flow_worker_scenarios.py +513 -0
- plaita-0.4.0/tests/test_from_file.py +18 -0
- plaita-0.4.0/tests/test_http.py +692 -0
- plaita-0.4.0/tests/test_inline.py +73 -0
- plaita-0.4.0/tests/test_io.py +358 -0
- plaita-0.4.0/tests/test_log_handler.py +193 -0
- plaita-0.4.0/tests/test_loop.py +464 -0
- plaita-0.4.0/tests/test_performance_benchmark.py +584 -0
- plaita-0.4.0/tests/test_redis.py +105 -0
- plaita-0.4.0/tests/test_registry.py +328 -0
- plaita-0.4.0/tests/test_storage.py +173 -0
- plaita-0.4.0/tests/test_storage_commons.py +163 -0
- plaita-0.4.0/tests/test_storage_main.py +59 -0
- plaita-0.4.0/tests/test_storage_redis.py +225 -0
- plaita-0.4.0/tests/test_types.py +58 -0
plaita-0.4.0/MANIFEST.in
ADDED
|
@@ -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)
|