wedata3-feature-engineering 0.2.6.dev2__tar.gz → 0.2.6.dev4__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.
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/PKG-INFO +6 -25
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/cloud_sdk_client/models.py +2 -2
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/constants/constants.py +1 -1
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/online_store/packed_codec.py +87 -6
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/feature_engineering/client.py +1 -1
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/feature_engineering/feast/infra/online_store/cleanup_packed.py +160 -4
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/feature_engineering/feast/infra/online_store/redis_packed.py +55 -15
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/feature_engineering/table_client/table_client.py +9 -9
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata3_feature_engineering.egg-info/PKG-INFO +6 -25
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata3_feature_engineering.egg-info/SOURCES.txt +0 -12
- wedata3_feature_engineering-0.2.6.dev2/tests/test_cleanup_packed.py +0 -265
- wedata3_feature_engineering-0.2.6.dev2/tests/test_client.py +0 -1722
- wedata3_feature_engineering-0.2.6.dev2/tests/test_common_utils.py +0 -8
- wedata3_feature_engineering-0.2.6.dev2/tests/test_feast_client_validate_keys.py +0 -190
- wedata3_feature_engineering-0.2.6.dev2/tests/test_feature_spec_training_set.py +0 -313
- wedata3_feature_engineering-0.2.6.dev2/tests/test_feature_store.py +0 -388
- wedata3_feature_engineering-0.2.6.dev2/tests/test_feature_table_client_validate_keys.py +0 -192
- wedata3_feature_engineering-0.2.6.dev2/tests/test_packed_codec.py +0 -380
- wedata3_feature_engineering-0.2.6.dev2/tests/test_redis_packed.py +0 -378
- wedata3_feature_engineering-0.2.6.dev2/tests/test_sync_template.py +0 -243
- wedata3_feature_engineering-0.2.6.dev2/tests/test_table_client.py +0 -154
- wedata3_feature_engineering-0.2.6.dev2/tests/test_table_client_dispatch.py +0 -223
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/README.md +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/setup.cfg +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/setup.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/base_table_client/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/base_table_client/base.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/cloud_sdk_client/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/cloud_sdk_client/client.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/cloud_sdk_client/utils.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/constants/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/constants/engine_types.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/entities/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/entities/column_info.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/entities/environment_variables.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/entities/feature.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/entities/feature_column_info.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/entities/feature_function.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/entities/feature_lookup.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/entities/feature_spec.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/entities/feature_spec_constants.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/entities/feature_table.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/entities/feature_table_info.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/entities/function_info.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/entities/on_demand_column_info.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/entities/source_data_column_info.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/entities/training_set.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/feast_client/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/feast_client/feast_client.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/log/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/log/logger.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/online_store/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/protos/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/protos/feature_store_pb2.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/spark_client/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/spark_client/spark_client.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/utils/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/utils/common_utils.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/utils/env_utils.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/utils/feature_lookup_utils.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/utils/feature_spec_utils.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/utils/feature_utils.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/utils/on_demand_utils.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/utils/schema_utils.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/utils/signature_utils.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/utils/topological_sort.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/utils/training_set_utils.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/utils/uc_utils.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/common/utils/validation_utils.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/feature_engineering/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/feature_engineering/feast/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/feature_engineering/feast/infra/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/feature_engineering/feast/infra/online_store/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/feature_engineering/feast/infra/online_store/postgres.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/feature_engineering/ml_training_client/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/feature_engineering/ml_training_client/ml_training_client.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/feature_engineering/mlflow_model.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/feature_engineering/table_client/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/feature_store/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/feature_store/client.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/feature_store/common/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/feature_store/common/store_config/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/feature_store/common/store_config/redis.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/feature_store/constants/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/feature_store/constants/engine_types.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/feature_store/feature_table_client/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/feature_store/feature_table_client/feature_table_client.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/feature_store/mlflow_model.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/feature_store/training_set_client/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/feature_store/training_set_client/training_set_client.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/tempo/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/tempo/interpol.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/tempo/intervals.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/tempo/io.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/tempo/ml.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/tempo/resample.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/tempo/tsdf.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata/tempo/utils.py +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata3_feature_engineering.egg-info/dependency_links.txt +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata3_feature_engineering.egg-info/requires.txt +0 -0
- {wedata3_feature_engineering-0.2.6.dev2 → wedata3-feature-engineering-0.2.6.dev4}/wedata3_feature_engineering.egg-info/top_level.txt +0 -0
|
@@ -1,41 +1,20 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: wedata3-feature-engineering
|
|
3
|
-
Version: 0.2.6.
|
|
3
|
+
Version: 0.2.6.dev4
|
|
4
4
|
Summary: Wedata Feature Engineering Library Development
|
|
5
|
-
Home-page:
|
|
5
|
+
Home-page: UNKNOWN
|
|
6
6
|
Author: meahqian
|
|
7
7
|
Author-email:
|
|
8
8
|
License: Apache 2.0
|
|
9
|
+
Platform: UNKNOWN
|
|
9
10
|
Classifier: Programming Language :: Python :: 3
|
|
10
11
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
11
12
|
Classifier: Operating System :: OS Independent
|
|
12
13
|
Requires-Python: >=3.10
|
|
13
14
|
Description-Content-Type: text/markdown
|
|
14
|
-
Requires-Dist: pandas>=1.0.0
|
|
15
|
-
Requires-Dist: feast[postgres,redis]==0.49.0
|
|
16
|
-
Requires-Dist: grpcio==1.74.0
|
|
17
|
-
Requires-Dist: msgpack<2,>=1.0
|
|
18
|
-
Requires-Dist: tencentcloud-sdk-python
|
|
19
|
-
Requires-Dist: ipython
|
|
20
|
-
Requires-Dist: msgpack<2.0.0,>=1.0.0
|
|
21
15
|
Provides-Extra: mlflow2
|
|
22
|
-
Requires-Dist: mlflow==2.17.2; extra == "mlflow2"
|
|
23
16
|
Provides-Extra: mlflow3
|
|
24
|
-
Requires-Dist: mlflow<3.11.0,>=3.10.0; extra == "mlflow3"
|
|
25
17
|
Provides-Extra: dev
|
|
26
|
-
Requires-Dist: flake8; extra == "dev"
|
|
27
|
-
Requires-Dist: pytest; extra == "dev"
|
|
28
|
-
Requires-Dist: python-dotenv; extra == "dev"
|
|
29
|
-
Requires-Dist: fakeredis[lua]; extra == "dev"
|
|
30
|
-
Dynamic: author
|
|
31
|
-
Dynamic: classifier
|
|
32
|
-
Dynamic: description
|
|
33
|
-
Dynamic: description-content-type
|
|
34
|
-
Dynamic: license
|
|
35
|
-
Dynamic: provides-extra
|
|
36
|
-
Dynamic: requires-dist
|
|
37
|
-
Dynamic: requires-python
|
|
38
|
-
Dynamic: summary
|
|
39
18
|
|
|
40
19
|
# wedata3-feature-engineering
|
|
41
20
|
|
|
@@ -160,3 +139,5 @@ bash build.sh
|
|
|
160
139
|
## 许可证
|
|
161
140
|
|
|
162
141
|
Apache License 2.0
|
|
142
|
+
|
|
143
|
+
|
|
@@ -1184,7 +1184,7 @@ class CreateOnlineTableRequest(AbstractModel):
|
|
|
1184
1184
|
TargetPath: 目标路径
|
|
1185
1185
|
OfflineFeatureConfiguration: 离线表信息
|
|
1186
1186
|
TaskSchedulerConfiguration: 任务调度配置
|
|
1187
|
-
OnlineStoreType: 在线存储类型(
|
|
1187
|
+
OnlineStoreType: 在线存储类型(TENCENT_POSTGRE | REDIS,缺省 TENCENT_POSTGRE)
|
|
1188
1188
|
"""
|
|
1189
1189
|
|
|
1190
1190
|
def __init__(self) -> None:
|
|
@@ -1351,7 +1351,7 @@ class DeleteOnlineTableRequest(AbstractModel):
|
|
|
1351
1351
|
WorkspaceId: 工作空间ID
|
|
1352
1352
|
TableNameInfo: 表信息
|
|
1353
1353
|
ResourceGroupId: 资源组ID
|
|
1354
|
-
OnlineStoreType: 在线存储类型(
|
|
1354
|
+
OnlineStoreType: 在线存储类型(TENCENT_POSTGRE | REDIS,缺省 TENCENT_POSTGRE)
|
|
1355
1355
|
"""
|
|
1356
1356
|
def __init__(self) -> None:
|
|
1357
1357
|
self._WorkspaceId = None
|
|
@@ -46,7 +46,7 @@ FEATURE_LOOKUP_CLIENT_PIP_PACKAGE = "wedata3-feature-engineering"
|
|
|
46
46
|
# 特征查找版本号
|
|
47
47
|
# FEATURE_LOOKUP_CLIENT_MAJOR_VERSION = "0.6.16"
|
|
48
48
|
# FEATURE_LOOKUP_CLIENT_PIP_PACKAGE_VERSION = f"{FEATURE_LOOKUP_CLIENT_PIP_PACKAGE}>=0.6.16"
|
|
49
|
-
FEATURE_LOOKUP_CLIENT_MAJOR_VERSION = "0.2.6.
|
|
49
|
+
FEATURE_LOOKUP_CLIENT_MAJOR_VERSION = "0.2.6.dev4"
|
|
50
50
|
FEATURE_LOOKUP_CLIENT_PIP_PACKAGE_VERSION = f"{FEATURE_LOOKUP_CLIENT_PIP_PACKAGE}>=0.2.4"
|
|
51
51
|
|
|
52
52
|
# 特征存储内部数据目录
|
|
@@ -50,6 +50,29 @@ _DATA_VERSION_RAND_LEN = 2
|
|
|
50
50
|
_OVERSIZE_WARNED: set = set()
|
|
51
51
|
|
|
52
52
|
|
|
53
|
+
def _check_msgpack_impl() -> None:
|
|
54
|
+
"""检测 msgpack 是否走 C 扩展实现(方案 D3.1.1 落地约束:防静默降级)。
|
|
55
|
+
|
|
56
|
+
zipimport 等场景下 msgpack 会静默退回纯 Python 实现,5 亿行同步慢数十倍。
|
|
57
|
+
Executor 上通过 ``spark.executorEnv.PYTHONPATH=./deps`` 分发的归档若平台 ABI
|
|
58
|
+
不匹配即触发该情况,必须在运行时告警而不是等同步超时才发现。
|
|
59
|
+
"""
|
|
60
|
+
try:
|
|
61
|
+
import msgpack
|
|
62
|
+
|
|
63
|
+
if "fallback" in str(msgpack.Packer):
|
|
64
|
+
logger.warning(
|
|
65
|
+
"msgpack is running in PURE-PYTHON fallback mode (C extension not loaded); "
|
|
66
|
+
"redis packed sync will be extremely slow. Check that the executor deps archive "
|
|
67
|
+
"matches the executor platform/ABI (see scripts/build_executor_deps.sh)."
|
|
68
|
+
)
|
|
69
|
+
except Exception: # pragma: no cover - 检测本身不允许影响主流程
|
|
70
|
+
logger.warning("msgpack implementation check skipped (import failed)", exc_info=True)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
_check_msgpack_impl()
|
|
74
|
+
|
|
75
|
+
|
|
53
76
|
class PackedCodecError(ValueError):
|
|
54
77
|
"""编解码层的显式失败(对应 codec 设计 §8 的 F2/F3/F8 等)。"""
|
|
55
78
|
|
|
@@ -389,19 +412,58 @@ def _encode_value(vtype: str, v: Any) -> Any:
|
|
|
389
412
|
return None
|
|
390
413
|
if isinstance(v, float) and math.isnan(v):
|
|
391
414
|
return None
|
|
392
|
-
if
|
|
415
|
+
if isinstance(v, Decimal):
|
|
416
|
+
return _encode_decimal(vtype, v)
|
|
417
|
+
if isinstance(v, _dt.date):
|
|
418
|
+
return _encode_temporal(vtype, v)
|
|
419
|
+
return v
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _encode_decimal(vtype: str, v: Decimal) -> Any:
|
|
423
|
+
"""Spark DecimalType → Decimal。Java 侧把 decimal 映射为 STRING(未知类型降级),msgpack 不能直接序列化 Decimal。"""
|
|
424
|
+
if not v.is_finite():
|
|
425
|
+
return None
|
|
426
|
+
if vtype == "STRING":
|
|
427
|
+
return format(v, "f") # 保留小数位、不出现科学计数法(Decimal('1E+2') → '100')
|
|
428
|
+
if vtype in ("DOUBLE", "FLOAT"):
|
|
429
|
+
return float(v)
|
|
430
|
+
if vtype in ("INT32", "INT64"):
|
|
431
|
+
if v != v.to_integral_value():
|
|
432
|
+
raise PackedCodecError(f"小数 {v} 不能写入整数类型 {vtype}")
|
|
433
|
+
return int(v)
|
|
434
|
+
raise PackedCodecError(f"Decimal 不能写入 {vtype} 类型")
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _encode_temporal(vtype: str, v: _dt.date) -> Any:
|
|
438
|
+
"""datetime / date。UNIX_TIMESTAMP 存 epoch 秒;Java 把 date 映射为 STRING,按 ISO 格式存。"""
|
|
439
|
+
if vtype == "UNIX_TIMESTAMP":
|
|
440
|
+
if not isinstance(v, _dt.datetime):
|
|
441
|
+
v = _dt.datetime(v.year, v.month, v.day) # date 按当天 0 点
|
|
393
442
|
if v.tzinfo is None:
|
|
394
443
|
v = v.replace(tzinfo=_dt.timezone.utc)
|
|
395
444
|
return int(v.timestamp())
|
|
396
|
-
|
|
445
|
+
if vtype == "STRING":
|
|
446
|
+
return v.isoformat()
|
|
447
|
+
raise PackedCodecError(f"{type(v).__name__} 不能写入 {vtype} 类型")
|
|
397
448
|
|
|
398
449
|
|
|
399
450
|
def encode_row(meta: SchemaMeta, ts_micros: Optional[int], values: Mapping[str, Any]) -> bytes:
|
|
400
451
|
"""dict → 位置数组 msgpack;缺失字段编码为 nil。下标 0 固定为 event_ts_micros。"""
|
|
401
452
|
arr: List[Any] = [None if ts_micros is None else int(ts_micros)]
|
|
402
453
|
for name, vtype in meta.fields:
|
|
403
|
-
|
|
404
|
-
|
|
454
|
+
try:
|
|
455
|
+
arr.append(_encode_value(vtype, values.get(name)))
|
|
456
|
+
except PackedCodecError as e:
|
|
457
|
+
raise PackedCodecError(f"特征 {name} 编码失败: {e}") from None
|
|
458
|
+
try:
|
|
459
|
+
return msgpack.packb(arr, use_bin_type=True)
|
|
460
|
+
except TypeError as e:
|
|
461
|
+
# 兜底:出现 codec 没覆盖的类型时报清楚是哪一列,而不是裸 msgpack 错误
|
|
462
|
+
bad = [n for (n, _), x in zip(meta.fields, arr[1:]) if not isinstance(x, _MSGPACK_SCALARS)]
|
|
463
|
+
raise PackedCodecError(f"特征 {bad} 的值类型不支持序列化: {e}") from None
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
_MSGPACK_SCALARS = (type(None), bool, int, float, str, bytes, bytearray)
|
|
405
467
|
|
|
406
468
|
|
|
407
469
|
def decode_row(
|
|
@@ -617,20 +679,35 @@ ACTIVATE_LUA = r"""
|
|
|
617
679
|
-- 返回 {switched(1/0), previous_active or "", reason}
|
|
618
680
|
local new_member = ARGV[1]
|
|
619
681
|
local now = tonumber(ARGV[2])
|
|
682
|
+
-- 水位超过 now + 1 天视为脏数据(未来时间戳),按 0 参与比较:
|
|
683
|
+
-- 否则一条脏数据会让之后所有批次都被判旧(与 WATERMARK_FUTURE_TOLERANCE_SECONDS 一致)
|
|
684
|
+
local MAX_FUTURE_SECONDS = 86400
|
|
685
|
+
local function watermark_of(info)
|
|
686
|
+
if info.watermark > now + MAX_FUTURE_SECONDS then
|
|
687
|
+
return 0
|
|
688
|
+
end
|
|
689
|
+
return info.watermark
|
|
690
|
+
end
|
|
620
691
|
|
|
621
692
|
local new_raw = redis.call('HGET', KEYS[2], new_member)
|
|
622
693
|
if not new_raw then
|
|
623
694
|
return {0, '', 'NEW_NOT_REGISTERED'}
|
|
624
695
|
end
|
|
625
696
|
local new_info = cjson.decode(new_raw)
|
|
697
|
+
-- 只激活写入中的批次:超时被判 ABANDONED、已 REJECTED / PURGED 的批次数据可能已被清理,激活即全量 miss
|
|
698
|
+
if new_info.state ~= 'WRITING' then
|
|
699
|
+
return {0, redis.call('GET', KEYS[1]) or '', 'NOT_WRITING'}
|
|
700
|
+
end
|
|
626
701
|
|
|
627
702
|
local cur_member = redis.call('GET', KEYS[1])
|
|
628
703
|
if cur_member then
|
|
629
704
|
local cur_raw = redis.call('HGET', KEYS[2], cur_member)
|
|
630
705
|
if cur_raw then
|
|
631
706
|
local cur = cjson.decode(cur_raw)
|
|
632
|
-
local
|
|
633
|
-
|
|
707
|
+
local new_wm = watermark_of(new_info)
|
|
708
|
+
local cur_wm = watermark_of(cur)
|
|
709
|
+
local newer = (new_wm > cur_wm) or
|
|
710
|
+
(new_wm == cur_wm and new_info.run_start > cur.run_start)
|
|
634
711
|
if not newer then
|
|
635
712
|
new_info.state = 'STALE'
|
|
636
713
|
new_info.updated_at = now
|
|
@@ -651,3 +728,7 @@ new_info.updated_at = now
|
|
|
651
728
|
redis.call('HSET', KEYS[2], new_member, cjson.encode(new_info))
|
|
652
729
|
return {1, cur_member or '', 'SWITCHED'}
|
|
653
730
|
"""
|
|
731
|
+
|
|
732
|
+
#: 批次水位允许超出当前时间的上限(秒)。同步模板登记时超出即截断为当前时间并告警,ACTIVATE_LUA 比较时超出按 0 处理。
|
|
733
|
+
#: 1 天能容纳时区错配(P1-13 的 8 小时偏移)与机器时钟偏差。
|
|
734
|
+
WATERMARK_FUTURE_TOLERANCE_SECONDS = 86400
|
|
@@ -1095,7 +1095,7 @@ class FeatureEngineeringClient:
|
|
|
1095
1095
|
由用户根据所在工作空间已配置的在线 catalog 显式指定(如 `DataLakeCatalog`),
|
|
1096
1096
|
SDK 不提供默认兜底以避免在多 catalog 场景下静默连接错误的 PG 实例。Redis 不传
|
|
1097
1097
|
trigger: [可选]周期性任务的调度配置
|
|
1098
|
-
online_store_type: [可选]"
|
|
1098
|
+
online_store_type: [可选]"TENCENT_POSTGRE"(默认)或 "REDIS"。Redis 在线表由离线表与
|
|
1099
1099
|
工作空间的 Redis 数据源确定,不需要也不接受在线库表名
|
|
1100
1100
|
"""
|
|
1101
1101
|
kwargs = {}
|
|
@@ -15,6 +15,7 @@ key 格式与 BatchInfo 编解码统一走 ``wedata.common.online_store.packed_c
|
|
|
15
15
|
| 状态 | 处置 |
|
|
16
16
|
|---|---|
|
|
17
17
|
| WRITING(未超时 <6h) | 不清理(同步进行中) |
|
|
18
|
+
| WRITING(作业失败收尾)| ``reject_batch`` 置 REJECTED 并立即清理,不等 6h |
|
|
18
19
|
| WRITING(超时 ≥6h)| 转 ABANDONED 后立即清理 |
|
|
19
20
|
| ACTIVE | **永不清理**(二次确认防误删) |
|
|
20
21
|
| RETIRED(最新 1 个,retired <1h) | 保留(回滚窗口) |
|
|
@@ -30,6 +31,7 @@ from __future__ import annotations
|
|
|
30
31
|
|
|
31
32
|
import logging
|
|
32
33
|
import re
|
|
34
|
+
import threading
|
|
33
35
|
import time
|
|
34
36
|
from typing import Any, Dict, List, Optional, Tuple
|
|
35
37
|
|
|
@@ -134,14 +136,15 @@ def cleanup(client: redis.Redis, prefix: str) -> int:
|
|
|
134
136
|
if state in (pc.STATE_REJECTED, pc.STATE_STALE, pc.STATE_ABANDONED):
|
|
135
137
|
to_delete.append((member, info))
|
|
136
138
|
elif state == pc.STATE_WRITING and _is_abandoned(info, now):
|
|
137
|
-
|
|
138
|
-
|
|
139
|
+
# 原子判定:期间批次被激活或续了心跳就不动它(快照判超时、再非原子改状态会把刚激活的批次删掉)
|
|
140
|
+
if _mark_abandoned(client, prefix, member, info, now):
|
|
141
|
+
to_delete.append((member, info))
|
|
139
142
|
# ACTIVE / 未超时 WRITING / PURGED:不删数据
|
|
140
143
|
|
|
141
144
|
deleted = 0
|
|
142
145
|
for member, _info in to_delete:
|
|
143
|
-
|
|
144
|
-
|
|
146
|
+
# active 以删除前实时读取为准,不用上面的快照:快照之后可能已切换
|
|
147
|
+
if member in (active_member, _get_active_member(client, prefix)):
|
|
145
148
|
logger.warning("skip purge of active batch %r on %r", member, prefix)
|
|
146
149
|
continue
|
|
147
150
|
try:
|
|
@@ -170,6 +173,149 @@ def purge_batch(client: redis.Redis, prefix: str, member: str) -> None:
|
|
|
170
173
|
_mark_state(client, prefix, member, pc.STATE_PURGED, time.time())
|
|
171
174
|
|
|
172
175
|
|
|
176
|
+
#: 只把仍处于 WRITING 的批次改为 REJECTED,返回改写后的状态(批次不存在返回空串)。
|
|
177
|
+
#: 原子判断:激活脚本已执行但客户端没收到回包时,批次可能已是 ACTIVE,绝不能被改写。
|
|
178
|
+
_REJECT_LUA = r"""
|
|
179
|
+
local raw = redis.call('HGET', KEYS[1], ARGV[1])
|
|
180
|
+
if not raw then
|
|
181
|
+
return ''
|
|
182
|
+
end
|
|
183
|
+
local info = cjson.decode(raw)
|
|
184
|
+
if info.state == 'WRITING' then
|
|
185
|
+
info.state = 'REJECTED'
|
|
186
|
+
info.updated_at = tonumber(ARGV[2])
|
|
187
|
+
info.reason = ARGV[3]
|
|
188
|
+
redis.call('HSET', KEYS[1], ARGV[1], cjson.encode(info))
|
|
189
|
+
end
|
|
190
|
+
return info.state
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
#: 失败收尾时可以立即删除数据的状态(REJECTED 来自本函数,STALE 来自激活脚本判旧)
|
|
194
|
+
_DISCARDABLE_STATES = frozenset({pc.STATE_REJECTED, pc.STATE_STALE})
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def reject_batch(client: redis.Redis, prefix: str, member: str, reason: str) -> bool:
|
|
198
|
+
"""
|
|
199
|
+
同步作业失败收尾:本批 WRITING → REJECTED,并立即删除本批数据,返回是否删除了数据。
|
|
200
|
+
|
|
201
|
+
不收尾的话批次停在 WRITING,下一次同步的门禁要等 ``MAX_RUN_HOURS`` 才放行;
|
|
202
|
+
激活被判旧(STALE)的批次同样立即删除,不留一份全量数据占内存到下一次同步。
|
|
203
|
+
已激活(ACTIVE / RETIRED)或已清理的批次不动。
|
|
204
|
+
"""
|
|
205
|
+
state = client.eval(
|
|
206
|
+
_REJECT_LUA, 1, pc.versions_key(prefix), member, int(time.time()), (reason or "")[:200]
|
|
207
|
+
)
|
|
208
|
+
state = state.decode() if isinstance(state, bytes) else state
|
|
209
|
+
if state not in _DISCARDABLE_STATES:
|
|
210
|
+
logger.warning("skip rejecting batch %r on %r: state=%r", member, prefix, state)
|
|
211
|
+
return False
|
|
212
|
+
purge_batch(client, prefix, member)
|
|
213
|
+
logger.info("rejected and purged batch %r on %r: %s", member, prefix, reason)
|
|
214
|
+
return True
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# ---------------------------------------------------------------------------
|
|
218
|
+
# 写入心跳(长时间同步不被判超时)
|
|
219
|
+
# ---------------------------------------------------------------------------
|
|
220
|
+
|
|
221
|
+
#: 心跳间隔,远小于 MAX_RUN_HOURS:Driver 卡顿几次也不至于被判超时
|
|
222
|
+
HEARTBEAT_INTERVAL_SECONDS = 300
|
|
223
|
+
|
|
224
|
+
#: 心跳最长持续时间:超过即停止续期,交给超时清理,防止失控的 Driver 永久占住门禁
|
|
225
|
+
HEARTBEAT_MAX_SECONDS = 24 * 3600
|
|
226
|
+
|
|
227
|
+
#: 只给仍是 WRITING 的批次刷新 updated_at,返回 1 / 0
|
|
228
|
+
_HEARTBEAT_LUA = r"""
|
|
229
|
+
local raw = redis.call('HGET', KEYS[1], ARGV[1])
|
|
230
|
+
if not raw then
|
|
231
|
+
return 0
|
|
232
|
+
end
|
|
233
|
+
local info = cjson.decode(raw)
|
|
234
|
+
if info.state ~= 'WRITING' then
|
|
235
|
+
return 0
|
|
236
|
+
end
|
|
237
|
+
info.updated_at = tonumber(ARGV[2])
|
|
238
|
+
redis.call('HSET', KEYS[1], ARGV[1], cjson.encode(info))
|
|
239
|
+
return 1
|
|
240
|
+
"""
|
|
241
|
+
|
|
242
|
+
#: 仍是 WRITING 且 updated_at 与判超时时看到的一致(期间没被激活、没续心跳)才改为 ABANDONED,返回 1 / 0
|
|
243
|
+
_MARK_ABANDONED_LUA = r"""
|
|
244
|
+
local raw = redis.call('HGET', KEYS[1], ARGV[1])
|
|
245
|
+
if not raw then
|
|
246
|
+
return 0
|
|
247
|
+
end
|
|
248
|
+
local info = cjson.decode(raw)
|
|
249
|
+
if info.state ~= 'WRITING' or (tonumber(info.updated_at) or 0) ~= tonumber(ARGV[2]) then
|
|
250
|
+
return 0
|
|
251
|
+
end
|
|
252
|
+
info.state = 'ABANDONED'
|
|
253
|
+
info.updated_at = tonumber(ARGV[3])
|
|
254
|
+
redis.call('HSET', KEYS[1], ARGV[1], cjson.encode(info))
|
|
255
|
+
return 1
|
|
256
|
+
"""
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class BatchHeartbeat:
|
|
260
|
+
"""
|
|
261
|
+
写入期间在后台线程里定期刷新批次 ``updated_at``。
|
|
262
|
+
|
|
263
|
+
不刷新的话,超过 ``MAX_RUN_HOURS`` 的同步会被下一次运行的门禁判为 ABANDONED 并删掉数据,
|
|
264
|
+
而本次运行随后仍可能激活它,``__active__`` 指向已删除的批次,读侧全量 miss。
|
|
265
|
+
批次一旦不再是 WRITING(已激活 / 被拒绝 / 被判超时),心跳自动结束。
|
|
266
|
+
"""
|
|
267
|
+
|
|
268
|
+
def __init__(
|
|
269
|
+
self,
|
|
270
|
+
client: redis.Redis,
|
|
271
|
+
prefix: str,
|
|
272
|
+
member: str,
|
|
273
|
+
interval_seconds: float = HEARTBEAT_INTERVAL_SECONDS,
|
|
274
|
+
max_seconds: float = HEARTBEAT_MAX_SECONDS,
|
|
275
|
+
) -> None:
|
|
276
|
+
self._client = client
|
|
277
|
+
self._prefix = prefix
|
|
278
|
+
self._member = member
|
|
279
|
+
self._interval = interval_seconds
|
|
280
|
+
self._max_seconds = max_seconds
|
|
281
|
+
self._stop = threading.Event()
|
|
282
|
+
self._thread = threading.Thread(target=self._run, name=f"batch-heartbeat-{member}", daemon=True)
|
|
283
|
+
|
|
284
|
+
def start(self) -> "BatchHeartbeat":
|
|
285
|
+
self._thread.start()
|
|
286
|
+
return self
|
|
287
|
+
|
|
288
|
+
def beat(self) -> bool:
|
|
289
|
+
"""刷新一次,返回批次是否仍在写入中。"""
|
|
290
|
+
return bool(
|
|
291
|
+
self._client.eval(_HEARTBEAT_LUA, 1, pc.versions_key(self._prefix), self._member, int(time.time()))
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
def stop(self) -> None:
|
|
295
|
+
"""停止心跳(幂等)。"""
|
|
296
|
+
self._stop.set()
|
|
297
|
+
if self._thread.is_alive() and self._thread is not threading.current_thread():
|
|
298
|
+
self._thread.join(timeout=5)
|
|
299
|
+
|
|
300
|
+
def _run(self) -> None:
|
|
301
|
+
deadline = time.monotonic() + self._max_seconds
|
|
302
|
+
while not self._stop.wait(self._interval):
|
|
303
|
+
if time.monotonic() > deadline:
|
|
304
|
+
logger.warning("heartbeat of batch %r on %r exceeded %ss, stop", self._member, self._prefix,
|
|
305
|
+
self._max_seconds)
|
|
306
|
+
return
|
|
307
|
+
try:
|
|
308
|
+
if not self.beat():
|
|
309
|
+
return
|
|
310
|
+
except Exception as exc: # 单次失败不退出:下一轮再试,超时判定有 MAX_RUN_HOURS 的余量
|
|
311
|
+
logger.warning("heartbeat of batch %r on %r failed: %s", self._member, self._prefix, exc)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def start_heartbeat(client: redis.Redis, prefix: str, member: str, **kwargs: Any) -> BatchHeartbeat:
|
|
315
|
+
"""登记批次后立即调用;激活或失败收尾前调用返回值的 ``stop()``。"""
|
|
316
|
+
return BatchHeartbeat(client, prefix, member, **kwargs).start()
|
|
317
|
+
|
|
318
|
+
|
|
173
319
|
# ---------------------------------------------------------------------------
|
|
174
320
|
# 整表删除(删除在线表的一次性清理作业 / SDK delete_table 共用)
|
|
175
321
|
# ---------------------------------------------------------------------------
|
|
@@ -273,6 +419,16 @@ def _is_abandoned(info: Dict[str, Any], now: float) -> bool:
|
|
|
273
419
|
return (now - updated) > MAX_RUN_HOURS * 3600
|
|
274
420
|
|
|
275
421
|
|
|
422
|
+
def _mark_abandoned(client: redis.Redis, prefix: str, member: str, seen: Dict[str, Any], now: float) -> bool:
|
|
423
|
+
"""超时的 WRITING → ABANDONED(原子比较 updated_at),返回是否标记成功。"""
|
|
424
|
+
return bool(
|
|
425
|
+
client.eval(
|
|
426
|
+
_MARK_ABANDONED_LUA, 1, pc.versions_key(prefix), member,
|
|
427
|
+
int(seen.get(pc.BATCH_UPDATED_AT, 0)), int(now),
|
|
428
|
+
)
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
|
|
276
432
|
def _get_active_member(client: redis.Redis, prefix: str) -> Optional[str]:
|
|
277
433
|
member = client.get(pc.active_key(prefix))
|
|
278
434
|
if member is None:
|
|
@@ -179,15 +179,61 @@ class WedataRedisPackedOnlineStore(OnlineStore):
|
|
|
179
179
|
# ------------------------------------------------------------------
|
|
180
180
|
|
|
181
181
|
@staticmethod
|
|
182
|
-
def serialize_entity_key(entity_key_proto: EntityKeyProto) -> str:
|
|
183
|
-
"""
|
|
182
|
+
def serialize_entity_key(entity_key_proto: EntityKeyProto, order: Optional[Sequence[int]] = None) -> str:
|
|
183
|
+
"""
|
|
184
|
+
EntityKeyProto → 实体键 ek(转义后用 ``|`` 连接,codec 设计 §2.6.3)。
|
|
185
|
+
|
|
186
|
+
:param order: 按 ``meta.entity_keys`` 顺序取值的下标(见 :meth:`_entity_value_order`);
|
|
187
|
+
None 表示 join_keys 顺序与 meta 一致,按原顺序取值
|
|
188
|
+
"""
|
|
184
189
|
values = []
|
|
185
190
|
for val in entity_key_proto.entity_values:
|
|
186
191
|
if val.WhichOneof("val") is None:
|
|
187
192
|
raise pc.PackedCodecError("entity value not set (None)")
|
|
188
193
|
values.append(_proto_value_to_py(val))
|
|
194
|
+
if order is not None:
|
|
195
|
+
values = [values[i] for i in order]
|
|
189
196
|
return pc.build_entity_key_from_values(values)
|
|
190
197
|
|
|
198
|
+
@staticmethod
|
|
199
|
+
def _entity_value_order(
|
|
200
|
+
join_keys: Sequence[str], meta: SchemaMeta, table_name: str, prefix: str
|
|
201
|
+
) -> Optional[List[int]]:
|
|
202
|
+
"""
|
|
203
|
+
F8:join_keys 与建表时的 ``meta.entity_keys`` 比对。
|
|
204
|
+
|
|
205
|
+
集合相同、顺序不同(如请求写成 ``{"b": 1, "a": 2}``,feast 按请求键序构造 join_keys)是合法请求,
|
|
206
|
+
返回按 meta 顺序取值的下标;顺序一致或未带 join_keys 返回 None;集合不同(registry 改名 / 传错 FV)抛错,
|
|
207
|
+
否则会拼出错误的 ek 静默 100% miss。
|
|
208
|
+
"""
|
|
209
|
+
if not join_keys:
|
|
210
|
+
return None
|
|
211
|
+
keys = tuple(join_keys)
|
|
212
|
+
expected = tuple(meta.entity_keys)
|
|
213
|
+
if keys == expected:
|
|
214
|
+
return None
|
|
215
|
+
if len(set(keys)) != len(keys) or set(keys) != set(expected):
|
|
216
|
+
raise pc.PackedCodecError(
|
|
217
|
+
f"entity join_keys {list(keys)} do not match online table "
|
|
218
|
+
f"{table_name} (meta entity_keys={list(expected)}, prefix={prefix!r}); "
|
|
219
|
+
f"the feature view / registry has drifted from the published table"
|
|
220
|
+
)
|
|
221
|
+
position = {k: i for i, k in enumerate(keys)}
|
|
222
|
+
return [position[k] for k in expected]
|
|
223
|
+
|
|
224
|
+
def _serialize_entity_keys(
|
|
225
|
+
self, entity_keys: Sequence[EntityKeyProto], meta: SchemaMeta, table_name: str, prefix: str
|
|
226
|
+
) -> List[str]:
|
|
227
|
+
"""按 meta 顺序批量构造 ek;同一批里 join_keys 顺序相同的只比对一次。"""
|
|
228
|
+
orders: Dict[Tuple[str, ...], Optional[List[int]]] = {}
|
|
229
|
+
out = []
|
|
230
|
+
for ek in entity_keys:
|
|
231
|
+
join_keys = tuple(ek.join_keys)
|
|
232
|
+
if join_keys not in orders:
|
|
233
|
+
orders[join_keys] = self._entity_value_order(join_keys, meta, table_name, prefix)
|
|
234
|
+
out.append(self.serialize_entity_key(ek, orders[join_keys]))
|
|
235
|
+
return out
|
|
236
|
+
|
|
191
237
|
# ------------------------------------------------------------------
|
|
192
238
|
# 读路径(稳态 1 次 MGET)
|
|
193
239
|
# ------------------------------------------------------------------
|
|
@@ -259,16 +305,8 @@ class WedataRedisPackedOnlineStore(OnlineStore):
|
|
|
259
305
|
sv, dv = pc.parse_active_pointer(member)
|
|
260
306
|
meta = self._get_meta(client, prefix, sv)
|
|
261
307
|
|
|
262
|
-
# F8:
|
|
263
|
-
|
|
264
|
-
if entity_keys[0].join_keys:
|
|
265
|
-
read_join_keys = tuple(entity_keys[0].join_keys)
|
|
266
|
-
if read_join_keys != tuple(meta.entity_keys):
|
|
267
|
-
raise pc.PackedCodecError(
|
|
268
|
-
f"entity join_keys {list(read_join_keys)} do not match online table "
|
|
269
|
-
f"{table.name} (meta entity_keys={list(meta.entity_keys)}, prefix={prefix!r}); "
|
|
270
|
-
f"the feature view / registry has drifted from the published table"
|
|
271
|
-
)
|
|
308
|
+
# F8:join_keys 与 meta.entity_keys 集合不同则抛错,顺序不同按 meta 顺序重排(见 _entity_value_order)
|
|
309
|
+
eks = self._serialize_entity_keys(entity_keys, meta, table.name, prefix)
|
|
272
310
|
|
|
273
311
|
# 校验 requested_features ⊆ meta fields(防拼写错误静默丢列)
|
|
274
312
|
if requested_features:
|
|
@@ -278,7 +316,7 @@ class WedataRedisPackedOnlineStore(OnlineStore):
|
|
|
278
316
|
f"requested features not in online table {table.name}: {sorted(unknown)}"
|
|
279
317
|
)
|
|
280
318
|
|
|
281
|
-
keys = [pc.data_key(prefix, sv, dv,
|
|
319
|
+
keys = [pc.data_key(prefix, sv, dv, ek) for ek in eks]
|
|
282
320
|
blobs = client.mget(keys)
|
|
283
321
|
|
|
284
322
|
result: List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]] = []
|
|
@@ -326,6 +364,8 @@ class WedataRedisPackedOnlineStore(OnlineStore):
|
|
|
326
364
|
meta = pc.build_meta_from_feature_view(table, source_table=_source_table(table))
|
|
327
365
|
ttl = int(getattr(config.online_store, "ttl_seconds", 86400 * 30))
|
|
328
366
|
max_bytes = int(getattr(config.online_store, "max_string_bytes", pc.DEFAULT_MAX_STRING_BYTES))
|
|
367
|
+
# 与读路径同一规则按 meta 顺序构造 ek;放在登记批次之前,主键非法时不留下 WRITING 批次
|
|
368
|
+
eks = self._serialize_entity_keys([d[0] for d in data], meta, table.name, prefix)
|
|
329
369
|
|
|
330
370
|
member = self._get_active(client, prefix)
|
|
331
371
|
with self._cache_lock: # 写路径直接失效读缓存,保证写后读一致
|
|
@@ -350,14 +390,14 @@ class WedataRedisPackedOnlineStore(OnlineStore):
|
|
|
350
390
|
member = pc.build_batch_member(sv, dv)
|
|
351
391
|
|
|
352
392
|
pipe = client.pipeline(transaction=False)
|
|
353
|
-
for
|
|
393
|
+
for ek, (_entity_key_proto, feature_map, event_ts, _created_ts) in zip(eks, data):
|
|
354
394
|
values = {
|
|
355
395
|
name: _proto_value_to_py(feature_map[name])
|
|
356
396
|
for name in meta.field_names
|
|
357
397
|
if name in feature_map
|
|
358
398
|
}
|
|
359
399
|
pc.validate_string_lengths(values, max_bytes)
|
|
360
|
-
key = pc.data_key(prefix, sv, dv,
|
|
400
|
+
key = pc.data_key(prefix, sv, dv, ek)
|
|
361
401
|
ts_micros = None if event_ts is None else event_time_to_micros(event_ts)
|
|
362
402
|
pipe.set(key, pc.encode_row(meta, ts_micros, values), ex=ttl)
|
|
363
403
|
pipe.execute()
|
|
@@ -813,7 +813,7 @@ class FeatureEngineeringTableClient(AbstractBaseTableClient):
|
|
|
813
813
|
Properties 中没有 connectionId,则降级走 TCCatalog 模式。常见取值如 `DataLakeCatalog`,
|
|
814
814
|
具体需结合所在工作空间已配置的在线 catalog 决定,禁止由 SDK 默认兜底。Redis 不传
|
|
815
815
|
trigger: [可选]周期性任务的调度配置
|
|
816
|
-
online_store_type: [可选]在线存储类型,"
|
|
816
|
+
online_store_type: [可选]在线存储类型,"TENCENT_POSTGRE"(默认)或 "REDIS"。
|
|
817
817
|
传 "REDIS" 时:在线表由离线表与 Redis 数据源确定,不需要在线库表名(传了即报错);
|
|
818
818
|
不支持持续同步(trigger 的 TriggerMode=CONTINUE_RUN 即报错),只支持 SNAPSHOT / TIME_TRIGGER;
|
|
819
819
|
同一离线表只能发布到 PG 或 Redis 其中一种(服务端校验);
|
|
@@ -830,11 +830,11 @@ class FeatureEngineeringTableClient(AbstractBaseTableClient):
|
|
|
830
830
|
"""
|
|
831
831
|
# store 类型分派(不传 = PG,行为与历史版本完全一致)
|
|
832
832
|
if online_store_type is None:
|
|
833
|
-
online_store_type = "
|
|
833
|
+
online_store_type = "TENCENT_POSTGRE"
|
|
834
834
|
online_store_type = online_store_type.upper()
|
|
835
|
-
if online_store_type not in ("
|
|
835
|
+
if online_store_type not in ("TENCENT_POSTGRE", "REDIS"):
|
|
836
836
|
raise ValueError(
|
|
837
|
-
f"online_store_type must be '
|
|
837
|
+
f"online_store_type must be 'TENCENT_POSTGRE' or 'REDIS', got {online_store_type!r}"
|
|
838
838
|
)
|
|
839
839
|
online_names = {
|
|
840
840
|
"online_db_name": online_db_name,
|
|
@@ -868,12 +868,12 @@ class FeatureEngineeringTableClient(AbstractBaseTableClient):
|
|
|
868
868
|
]
|
|
869
869
|
if given_redis:
|
|
870
870
|
raise ValueError(
|
|
871
|
-
f"{', '.join(given_redis)} must not be set for
|
|
871
|
+
f"{', '.join(given_redis)} must not be set for TENCENT_POSTGRE: "
|
|
872
872
|
f"the PG connection is resolved from online_catalog_name on the server side"
|
|
873
873
|
)
|
|
874
874
|
missing = [k for k, v in online_names.items() if not v]
|
|
875
875
|
if missing:
|
|
876
|
-
raise ValueError(f"{', '.join(missing)} is required for
|
|
876
|
+
raise ValueError(f"{', '.join(missing)} is required for TENCENT_POSTGRE online table")
|
|
877
877
|
|
|
878
878
|
# 构建完整表名
|
|
879
879
|
full_table_name = common_utils.build_full_table_name(table_name, schema_name, catalog_name, self._spark)
|
|
@@ -1037,16 +1037,16 @@ class FeatureEngineeringTableClient(AbstractBaseTableClient):
|
|
|
1037
1037
|
catalog_name: 数据目录名称
|
|
1038
1038
|
schema_name: 数据库名称
|
|
1039
1039
|
table_name: 表名
|
|
1040
|
-
online_store_type: [可选]在线存储类型,"
|
|
1040
|
+
online_store_type: [可选]在线存储类型,"TENCENT_POSTGRE" 或 "REDIS"。不传时由服务端按标签判定;
|
|
1041
1041
|
传了也以标签为准(不一致时服务端告警)。
|
|
1042
1042
|
Raises:
|
|
1043
1043
|
RuntimeError: 当删除操作失败时抛出
|
|
1044
1044
|
"""
|
|
1045
1045
|
if online_store_type is not None:
|
|
1046
1046
|
online_store_type = online_store_type.upper()
|
|
1047
|
-
if online_store_type not in ("
|
|
1047
|
+
if online_store_type not in ("TENCENT_POSTGRE", "REDIS"):
|
|
1048
1048
|
raise ValueError(
|
|
1049
|
-
f"online_store_type must be '
|
|
1049
|
+
f"online_store_type must be 'TENCENT_POSTGRE' or 'REDIS', got {online_store_type!r}"
|
|
1050
1050
|
)
|
|
1051
1051
|
|
|
1052
1052
|
# 接口分页参数
|
|
@@ -1,41 +1,20 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: wedata3-feature-engineering
|
|
3
|
-
Version: 0.2.6.
|
|
3
|
+
Version: 0.2.6.dev4
|
|
4
4
|
Summary: Wedata Feature Engineering Library Development
|
|
5
|
-
Home-page:
|
|
5
|
+
Home-page: UNKNOWN
|
|
6
6
|
Author: meahqian
|
|
7
7
|
Author-email:
|
|
8
8
|
License: Apache 2.0
|
|
9
|
+
Platform: UNKNOWN
|
|
9
10
|
Classifier: Programming Language :: Python :: 3
|
|
10
11
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
11
12
|
Classifier: Operating System :: OS Independent
|
|
12
13
|
Requires-Python: >=3.10
|
|
13
14
|
Description-Content-Type: text/markdown
|
|
14
|
-
Requires-Dist: pandas>=1.0.0
|
|
15
|
-
Requires-Dist: feast[postgres,redis]==0.49.0
|
|
16
|
-
Requires-Dist: grpcio==1.74.0
|
|
17
|
-
Requires-Dist: msgpack<2,>=1.0
|
|
18
|
-
Requires-Dist: tencentcloud-sdk-python
|
|
19
|
-
Requires-Dist: ipython
|
|
20
|
-
Requires-Dist: msgpack<2.0.0,>=1.0.0
|
|
21
15
|
Provides-Extra: mlflow2
|
|
22
|
-
Requires-Dist: mlflow==2.17.2; extra == "mlflow2"
|
|
23
16
|
Provides-Extra: mlflow3
|
|
24
|
-
Requires-Dist: mlflow<3.11.0,>=3.10.0; extra == "mlflow3"
|
|
25
17
|
Provides-Extra: dev
|
|
26
|
-
Requires-Dist: flake8; extra == "dev"
|
|
27
|
-
Requires-Dist: pytest; extra == "dev"
|
|
28
|
-
Requires-Dist: python-dotenv; extra == "dev"
|
|
29
|
-
Requires-Dist: fakeredis[lua]; extra == "dev"
|
|
30
|
-
Dynamic: author
|
|
31
|
-
Dynamic: classifier
|
|
32
|
-
Dynamic: description
|
|
33
|
-
Dynamic: description-content-type
|
|
34
|
-
Dynamic: license
|
|
35
|
-
Dynamic: provides-extra
|
|
36
|
-
Dynamic: requires-dist
|
|
37
|
-
Dynamic: requires-python
|
|
38
|
-
Dynamic: summary
|
|
39
18
|
|
|
40
19
|
# wedata3-feature-engineering
|
|
41
20
|
|
|
@@ -160,3 +139,5 @@ bash build.sh
|
|
|
160
139
|
## 许可证
|
|
161
140
|
|
|
162
141
|
Apache License 2.0
|
|
142
|
+
|
|
143
|
+
|