mlflow-tclake-plugin 2.1.7.dev5__tar.gz → 2.1.7.dev6__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.
- {mlflow_tclake_plugin-2.1.7.dev5 → mlflow_tclake_plugin-2.1.7.dev6}/PKG-INFO +1 -1
- mlflow_tclake_plugin-2.1.7.dev6/mlflow_tclake_plugin/credential_provider.py +466 -0
- {mlflow_tclake_plugin-2.1.7.dev5 → mlflow_tclake_plugin-2.1.7.dev6}/mlflow_tclake_plugin/tclake_store.py +18 -28
- {mlflow_tclake_plugin-2.1.7.dev5 → mlflow_tclake_plugin-2.1.7.dev6}/mlflow_tclake_plugin.egg-info/PKG-INFO +1 -1
- {mlflow_tclake_plugin-2.1.7.dev5 → mlflow_tclake_plugin-2.1.7.dev6}/mlflow_tclake_plugin.egg-info/SOURCES.txt +1 -0
- {mlflow_tclake_plugin-2.1.7.dev5 → mlflow_tclake_plugin-2.1.7.dev6}/setup.py +1 -1
- {mlflow_tclake_plugin-2.1.7.dev5 → mlflow_tclake_plugin-2.1.7.dev6}/LICENSE.txt +0 -0
- {mlflow_tclake_plugin-2.1.7.dev5 → mlflow_tclake_plugin-2.1.7.dev6}/README.md +0 -0
- {mlflow_tclake_plugin-2.1.7.dev5 → mlflow_tclake_plugin-2.1.7.dev6}/mlflow_tclake_plugin/__init__.py +0 -0
- {mlflow_tclake_plugin-2.1.7.dev5 → mlflow_tclake_plugin-2.1.7.dev6}/mlflow_tclake_plugin.egg-info/dependency_links.txt +0 -0
- {mlflow_tclake_plugin-2.1.7.dev5 → mlflow_tclake_plugin-2.1.7.dev6}/mlflow_tclake_plugin.egg-info/entry_points.txt +0 -0
- {mlflow_tclake_plugin-2.1.7.dev5 → mlflow_tclake_plugin-2.1.7.dev6}/mlflow_tclake_plugin.egg-info/requires.txt +0 -0
- {mlflow_tclake_plugin-2.1.7.dev5 → mlflow_tclake_plugin-2.1.7.dev6}/mlflow_tclake_plugin.egg-info/top_level.txt +0 -0
- {mlflow_tclake_plugin-2.1.7.dev5 → mlflow_tclake_plugin-2.1.7.dev6}/setup.cfg +0 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
"""
|
|
2
|
+
腾讯云凭证提供器(自包含,不依赖 wedata_automl)
|
|
3
|
+
|
|
4
|
+
密钥优先级(高 -> 低):
|
|
5
|
+
|
|
6
|
+
1. 固定密钥:`TENCENTCLOUD_SECRET_ID` + `TENCENTCLOUD_SECRET_KEY` 且**没有**
|
|
7
|
+
`TENCENTCLOUD_TOKEN` —— 视为长期密钥,直接使用,不做续期、不启动定时任务。
|
|
8
|
+
2. 临时密钥:以环境变量中的密钥(含 `TENCENTCLOUD_TOKEN`)作为引导凭证,
|
|
9
|
+
用 `CommonClient` 调用 wedata 云 API `DescribeCamTemporaryToken` 换取临时密钥,
|
|
10
|
+
并由后台定时任务在**过期前 60 秒**自动续期。
|
|
11
|
+
|
|
12
|
+
为什么续期必须用定时任务,而不是"用的时候再检查"
|
|
13
|
+
--------------------------------------------------
|
|
14
|
+
续期本身也是一次需要签名的云 API 调用,用的是当前这把临时密钥。一旦它彻底过期,
|
|
15
|
+
续期请求会因鉴权失败而无法完成,凭证链就永久断裂。所以必须在过期前主动续期 ——
|
|
16
|
+
即使这段时间内业务侧完全没有调用云 API。
|
|
17
|
+
|
|
18
|
+
ProductName 说明
|
|
19
|
+
----------------
|
|
20
|
+
`DescribeCamTemporaryToken` 的 `ProductName` 仅支持 `wedata` 与 `tccatalog`,
|
|
21
|
+
服务端据此下发 `name/{ProductName}:*` 的授权策略,两者的 token 不能互用。
|
|
22
|
+
本插件访问 tccatalog,因此固定使用 `tccatalog`。
|
|
23
|
+
|
|
24
|
+
注意:`DescribeCamTemporaryToken` 本身始终发往 **wedata** 服务端点
|
|
25
|
+
(`TENCENTCLOUD_ENDPOINT` 指向的是 tccatalog,不能用于这个调用)。
|
|
26
|
+
|
|
27
|
+
注意:本模块刻意不依赖 `wedata_automl`——本插件是被 wedata_automl 安装使用的,
|
|
28
|
+
反向依赖会形成循环。
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
import os
|
|
32
|
+
import threading
|
|
33
|
+
import time
|
|
34
|
+
|
|
35
|
+
ENV_SECRET_ID = "TENCENTCLOUD_SECRET_ID"
|
|
36
|
+
ENV_SECRET_KEY = "TENCENTCLOUD_SECRET_KEY"
|
|
37
|
+
ENV_TOKEN = "TENCENTCLOUD_TOKEN"
|
|
38
|
+
ENV_TMP_TOKEN = "TENCENTCLOUD_TMP_TOKEN"
|
|
39
|
+
ENV_PROJECT_ID = "WEDATA_PROJECT_ID"
|
|
40
|
+
ENV_WEDATA_ENDPOINT = "TENCENT_CLOUD_SDK_ENDPOINT"
|
|
41
|
+
ENV_REFRESH_AHEAD = "WEDATA_SECRET_REFRESH_AHEAD_SECONDS"
|
|
42
|
+
ENV_DISABLE_DYNAMIC = "WEDATA_DISABLE_DYNAMIC_SECRET"
|
|
43
|
+
|
|
44
|
+
PRODUCT_WEDATA = "wedata"
|
|
45
|
+
PRODUCT_TCCATALOG = "tccatalog"
|
|
46
|
+
|
|
47
|
+
WEDATA_SERVICE = "wedata"
|
|
48
|
+
WEDATA_API_VERSION = "2021-08-20"
|
|
49
|
+
DEFAULT_WEDATA_ENDPOINT = "wedata.internal.tencentcloudapi.com"
|
|
50
|
+
|
|
51
|
+
DEFAULT_REFRESH_AHEAD_SECONDS = 60
|
|
52
|
+
MIN_REFRESH_DELAY_SECONDS = 5
|
|
53
|
+
RETRY_DELAY_SECONDS = 30
|
|
54
|
+
# ExpiredTime 合法区间(秒级时间戳):2020-01-01 ~ 2100-01-01
|
|
55
|
+
MIN_VALID_EPOCH_SECONDS = 1577836800
|
|
56
|
+
MAX_VALID_EPOCH_SECONDS = 4102444800
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _debug(msg):
|
|
60
|
+
if os.getenv("TENCENTCLOUD_DEBUG", None):
|
|
61
|
+
print(msg)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def mask_secret_id(secret_id):
|
|
65
|
+
"""脱敏展示 SecretId,仅保留前后各 4 位"""
|
|
66
|
+
if not secret_id:
|
|
67
|
+
return ""
|
|
68
|
+
if len(secret_id) <= 8:
|
|
69
|
+
return "*" * len(secret_id)
|
|
70
|
+
return "{}{}{}".format(secret_id[:4], "*" * 6, secret_id[-4:])
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _read_refresh_ahead():
|
|
74
|
+
try:
|
|
75
|
+
value = int(os.getenv(ENV_REFRESH_AHEAD, str(DEFAULT_REFRESH_AHEAD_SECONDS)))
|
|
76
|
+
return value if value >= 0 else DEFAULT_REFRESH_AHEAD_SECONDS
|
|
77
|
+
except (TypeError, ValueError):
|
|
78
|
+
return DEFAULT_REFRESH_AHEAD_SECONDS
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _dynamic_disabled():
|
|
82
|
+
"""是否禁用"调云 API 换临时密钥"(逃生阀)"""
|
|
83
|
+
return str(os.getenv(ENV_DISABLE_DYNAMIC, "")).lower() in ("1", "true", "yes")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _parse_expire(value):
|
|
87
|
+
"""
|
|
88
|
+
解析 ExpiredTime
|
|
89
|
+
|
|
90
|
+
`DescribeCamTemporaryToken` 返回的 `ExpiredTime` 是**秒级绝对时间戳**
|
|
91
|
+
(例如 1788789863 -> 2026-09-07 22:04:23)。
|
|
92
|
+
|
|
93
|
+
只做解析与合法性校验,不做语义猜测:异常值告警并返回 0,
|
|
94
|
+
由上层按"没有到期信息"处理,避免静默换算把问题藏起来。
|
|
95
|
+
"""
|
|
96
|
+
if value is None:
|
|
97
|
+
return 0
|
|
98
|
+
try:
|
|
99
|
+
expire = int(value)
|
|
100
|
+
except (TypeError, ValueError):
|
|
101
|
+
_debug("invalid ExpiredTime (not an int): {!r}".format(value))
|
|
102
|
+
return 0
|
|
103
|
+
if expire <= 0:
|
|
104
|
+
_debug("invalid ExpiredTime (non-positive): {}".format(expire))
|
|
105
|
+
return 0
|
|
106
|
+
if expire < MIN_VALID_EPOCH_SECONDS or expire > MAX_VALID_EPOCH_SECONDS:
|
|
107
|
+
_debug("ExpiredTime {} is not a plausible epoch-seconds value; "
|
|
108
|
+
"treating as unknown".format(expire))
|
|
109
|
+
return 0
|
|
110
|
+
return expire
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class TempCredential(object):
|
|
114
|
+
"""密钥快照"""
|
|
115
|
+
|
|
116
|
+
__slots__ = ("secret_id", "secret_key", "token", "expire", "owner_uin", "user_id")
|
|
117
|
+
|
|
118
|
+
def __init__(self, secret_id=None, secret_key=None, token=None, expire=0,
|
|
119
|
+
owner_uin=None, user_id=None):
|
|
120
|
+
self.secret_id = secret_id or None
|
|
121
|
+
self.secret_key = secret_key or None
|
|
122
|
+
self.token = token or None
|
|
123
|
+
self.expire = int(expire or 0)
|
|
124
|
+
self.owner_uin = owner_uin or None
|
|
125
|
+
self.user_id = user_id or None
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def valid(self):
|
|
129
|
+
return bool(self.secret_id and self.secret_key)
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def is_static(self):
|
|
133
|
+
"""无 token 即视为固定(长期)密钥"""
|
|
134
|
+
return self.valid and not self.token
|
|
135
|
+
|
|
136
|
+
def identity(self):
|
|
137
|
+
return self.secret_id, self.secret_key, self.token, self.expire
|
|
138
|
+
|
|
139
|
+
def is_expiring(self, refresh_ahead_seconds=DEFAULT_REFRESH_AHEAD_SECONDS):
|
|
140
|
+
if self.expire <= 0:
|
|
141
|
+
return False
|
|
142
|
+
return time.time() >= (self.expire - max(refresh_ahead_seconds, 0))
|
|
143
|
+
|
|
144
|
+
def seconds_to_expire(self):
|
|
145
|
+
if self.expire <= 0:
|
|
146
|
+
return None
|
|
147
|
+
return int(self.expire - time.time())
|
|
148
|
+
|
|
149
|
+
def __repr__(self): # 不输出 SecretKey / Token
|
|
150
|
+
return "TempCredential(secret_id={}, has_token={}, expire={}, ttl={})".format(
|
|
151
|
+
mask_secret_id(self.secret_id), bool(self.token), self.expire,
|
|
152
|
+
self.seconds_to_expire()
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class CredentialProvider(object):
|
|
157
|
+
"""
|
|
158
|
+
凭证提供器
|
|
159
|
+
|
|
160
|
+
- 固定密钥:直接返回,不启动定时任务
|
|
161
|
+
- 临时密钥:调用 `DescribeCamTemporaryToken` 换取,并由后台定时任务提前续期
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
def __init__(self, product_name=PRODUCT_TCCATALOG, project_id=None, region=None,
|
|
165
|
+
refresh_ahead_seconds=None):
|
|
166
|
+
self._product_name = product_name or PRODUCT_TCCATALOG
|
|
167
|
+
self._project_id = project_id or os.getenv(ENV_PROJECT_ID) or None
|
|
168
|
+
self._region = region
|
|
169
|
+
self._refresh_ahead_seconds = (
|
|
170
|
+
refresh_ahead_seconds if refresh_ahead_seconds is not None else _read_refresh_ahead()
|
|
171
|
+
)
|
|
172
|
+
self._base = TempCredential(
|
|
173
|
+
secret_id=os.getenv(ENV_SECRET_ID) or None,
|
|
174
|
+
secret_key=os.getenv(ENV_SECRET_KEY) or None,
|
|
175
|
+
token=os.getenv(ENV_TOKEN) or os.getenv(ENV_TMP_TOKEN) or None,
|
|
176
|
+
expire=0,
|
|
177
|
+
)
|
|
178
|
+
self._cached = None
|
|
179
|
+
self._timer = None
|
|
180
|
+
self._stopped = False
|
|
181
|
+
self._lock = threading.RLock()
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def product_name(self):
|
|
185
|
+
return self._product_name
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def use_static_secret(self):
|
|
189
|
+
"""固定密钥(无 token),或逃生阀开启 -> 不调云 API 换密钥"""
|
|
190
|
+
return self._base.is_static or _dynamic_disabled()
|
|
191
|
+
|
|
192
|
+
def get(self, force_refresh=False):
|
|
193
|
+
"""获取当前可用密钥;临时密钥在过期/即将过期时同步续期一次(兜底)"""
|
|
194
|
+
if self.use_static_secret:
|
|
195
|
+
return self._base
|
|
196
|
+
|
|
197
|
+
with self._lock:
|
|
198
|
+
cached = self._cached
|
|
199
|
+
if (
|
|
200
|
+
not force_refresh
|
|
201
|
+
and cached is not None
|
|
202
|
+
and cached.valid
|
|
203
|
+
and not cached.is_expiring(self._refresh_ahead_seconds)
|
|
204
|
+
):
|
|
205
|
+
return cached
|
|
206
|
+
|
|
207
|
+
refreshed = self._refresh_locked()
|
|
208
|
+
if refreshed is not None:
|
|
209
|
+
return refreshed
|
|
210
|
+
if cached is not None and cached.valid:
|
|
211
|
+
return cached
|
|
212
|
+
return self._base
|
|
213
|
+
|
|
214
|
+
def start_auto_refresh(self):
|
|
215
|
+
"""启动后台定时续期任务(幂等);固定密钥场景不创建线程"""
|
|
216
|
+
if self.use_static_secret:
|
|
217
|
+
return False
|
|
218
|
+
|
|
219
|
+
with self._lock:
|
|
220
|
+
self._stopped = False
|
|
221
|
+
if self._timer is not None:
|
|
222
|
+
return True
|
|
223
|
+
self._refresh_locked()
|
|
224
|
+
self._schedule_locked()
|
|
225
|
+
return True
|
|
226
|
+
|
|
227
|
+
def stop_auto_refresh(self):
|
|
228
|
+
with self._lock:
|
|
229
|
+
self._stopped = True
|
|
230
|
+
if self._timer is not None:
|
|
231
|
+
self._timer.cancel()
|
|
232
|
+
self._timer = None
|
|
233
|
+
|
|
234
|
+
def _schedule_locked(self):
|
|
235
|
+
if self._stopped:
|
|
236
|
+
return
|
|
237
|
+
cached = self._cached
|
|
238
|
+
if cached is not None and cached.valid and cached.expire > 0:
|
|
239
|
+
ttl = cached.expire - time.time()
|
|
240
|
+
delay = ttl - self._refresh_ahead_seconds
|
|
241
|
+
if delay < MIN_REFRESH_DELAY_SECONDS:
|
|
242
|
+
# TTL 比提前量还短:固定用最小延迟会变成每 5s 打一次云 API(可能限频),
|
|
243
|
+
# 退化为按 TTL 的一半续期
|
|
244
|
+
delay = max(ttl / 2.0, MIN_REFRESH_DELAY_SECONDS)
|
|
245
|
+
_debug("credential ttl ({}s) shorter than refresh-ahead ({}s), "
|
|
246
|
+
"next refresh in {}s".format(
|
|
247
|
+
int(ttl), self._refresh_ahead_seconds, int(delay)))
|
|
248
|
+
else:
|
|
249
|
+
delay = RETRY_DELAY_SECONDS
|
|
250
|
+
|
|
251
|
+
timer = threading.Timer(delay, self._on_timer)
|
|
252
|
+
timer.daemon = True
|
|
253
|
+
timer.name = "tclake-cred-refresh"
|
|
254
|
+
timer.start()
|
|
255
|
+
self._timer = timer
|
|
256
|
+
_debug("next credential refresh in {}s".format(int(delay)))
|
|
257
|
+
|
|
258
|
+
def _on_timer(self):
|
|
259
|
+
with self._lock:
|
|
260
|
+
self._timer = None
|
|
261
|
+
if self._stopped:
|
|
262
|
+
return
|
|
263
|
+
try:
|
|
264
|
+
self._refresh_locked()
|
|
265
|
+
except Exception as e:
|
|
266
|
+
_debug("scheduled credential refresh failed: {}".format(e))
|
|
267
|
+
self._schedule_locked()
|
|
268
|
+
|
|
269
|
+
def _refresh_locked(self):
|
|
270
|
+
fetched = self._fetch_cam_token()
|
|
271
|
+
if fetched is None or not fetched.valid:
|
|
272
|
+
return None
|
|
273
|
+
changed = self._cached is None or self._cached.identity() != fetched.identity()
|
|
274
|
+
self._cached = fetched
|
|
275
|
+
self.sync_tencentcloud_env(fetched)
|
|
276
|
+
if changed:
|
|
277
|
+
_debug("cam temporary credential refreshed: {}".format(fetched))
|
|
278
|
+
return fetched
|
|
279
|
+
|
|
280
|
+
def _signing_credential(self):
|
|
281
|
+
"""构造调用 DescribeCamTemporaryToken 的签名凭证(链式续期)"""
|
|
282
|
+
from tencentcloud.common import credential as tc_credential
|
|
283
|
+
|
|
284
|
+
cached = self._cached
|
|
285
|
+
if cached is not None and cached.valid and not cached.is_expiring(0):
|
|
286
|
+
source = cached
|
|
287
|
+
else:
|
|
288
|
+
source = self._base
|
|
289
|
+
return tc_credential.Credential(source.secret_id, source.secret_key, source.token)
|
|
290
|
+
|
|
291
|
+
def _fetch_cam_token(self):
|
|
292
|
+
if not self._base.valid:
|
|
293
|
+
_debug("cannot fetch cam temporary token: secret id/key not set")
|
|
294
|
+
return None
|
|
295
|
+
if not self._project_id:
|
|
296
|
+
_debug("cannot fetch cam temporary token: WEDATA_PROJECT_ID not set")
|
|
297
|
+
return None
|
|
298
|
+
if not self._region:
|
|
299
|
+
_debug("cannot fetch cam temporary token: region unknown")
|
|
300
|
+
return None
|
|
301
|
+
|
|
302
|
+
try:
|
|
303
|
+
from tencentcloud.common.common_client import CommonClient
|
|
304
|
+
|
|
305
|
+
client = CommonClient(
|
|
306
|
+
WEDATA_SERVICE,
|
|
307
|
+
WEDATA_API_VERSION,
|
|
308
|
+
self._signing_credential(),
|
|
309
|
+
self._region,
|
|
310
|
+
self._build_wedata_profile(),
|
|
311
|
+
)
|
|
312
|
+
params = {"ProjectId": self._project_id, "ProductName": self._product_name}
|
|
313
|
+
resp = client.call_json("DescribeCamTemporaryToken", params)
|
|
314
|
+
except Exception as e:
|
|
315
|
+
_debug("call DescribeCamTemporaryToken failed: {}".format(e))
|
|
316
|
+
return None
|
|
317
|
+
|
|
318
|
+
data = (resp or {}).get("Response", {}).get("Data") or {}
|
|
319
|
+
secret_id = data.get("SecretId")
|
|
320
|
+
secret_key = data.get("SecretKey")
|
|
321
|
+
if not secret_id or not secret_key:
|
|
322
|
+
_debug("DescribeCamTemporaryToken returned no credential")
|
|
323
|
+
return None
|
|
324
|
+
|
|
325
|
+
return TempCredential(
|
|
326
|
+
secret_id=secret_id,
|
|
327
|
+
secret_key=secret_key,
|
|
328
|
+
token=data.get("Token"),
|
|
329
|
+
expire=_parse_expire(data.get("ExpiredTime")),
|
|
330
|
+
owner_uin=data.get("OwnerUserId"),
|
|
331
|
+
user_id=data.get("UserId"),
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
@staticmethod
|
|
335
|
+
def _build_wedata_profile():
|
|
336
|
+
"""
|
|
337
|
+
构造调用 wedata 云 API 的 ClientProfile
|
|
338
|
+
|
|
339
|
+
不能复用 TENCENTCLOUD_ENDPOINT——那个指向 tccatalog。
|
|
340
|
+
"""
|
|
341
|
+
from tencentcloud.common.profile.client_profile import ClientProfile
|
|
342
|
+
from tencentcloud.common.profile.http_profile import HttpProfile
|
|
343
|
+
|
|
344
|
+
http_profile = HttpProfile()
|
|
345
|
+
http_profile.protocol = "https"
|
|
346
|
+
http_profile.endpoint = os.getenv(ENV_WEDATA_ENDPOINT) or DEFAULT_WEDATA_ENDPOINT
|
|
347
|
+
client_profile = ClientProfile()
|
|
348
|
+
client_profile.httpProfile = http_profile
|
|
349
|
+
return client_profile
|
|
350
|
+
|
|
351
|
+
@staticmethod
|
|
352
|
+
def sync_tencentcloud_env(cred):
|
|
353
|
+
"""把最新密钥同步到 TENCENTCLOUD_* 环境变量"""
|
|
354
|
+
if not cred.valid:
|
|
355
|
+
return
|
|
356
|
+
os.environ[ENV_SECRET_ID] = cred.secret_id
|
|
357
|
+
os.environ[ENV_SECRET_KEY] = cred.secret_key
|
|
358
|
+
if cred.token:
|
|
359
|
+
os.environ[ENV_TOKEN] = cred.token
|
|
360
|
+
os.environ[ENV_TMP_TOKEN] = cred.token
|
|
361
|
+
|
|
362
|
+
def as_sdk_credential(self):
|
|
363
|
+
"""包装为腾讯云 SDK 可直接使用的动态凭证"""
|
|
364
|
+
return DynamicCredential(self)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
class DynamicCredential(object):
|
|
368
|
+
"""
|
|
369
|
+
腾讯云 SDK 动态凭证
|
|
370
|
+
|
|
371
|
+
SDK 每次请求签名(含每次重试)都会调用 `get_credential_info()`,
|
|
372
|
+
因此 client 无需重建即可用上续期后的密钥。
|
|
373
|
+
"""
|
|
374
|
+
|
|
375
|
+
def __init__(self, provider):
|
|
376
|
+
self._provider = provider
|
|
377
|
+
|
|
378
|
+
def _current(self):
|
|
379
|
+
cred = self._provider.get()
|
|
380
|
+
if not cred.valid:
|
|
381
|
+
raise _credential_error(
|
|
382
|
+
"no valid tencentcloud credential: {}/{} not set and "
|
|
383
|
+
"DescribeCamTemporaryToken unavailable".format(ENV_SECRET_ID, ENV_SECRET_KEY)
|
|
384
|
+
)
|
|
385
|
+
return cred
|
|
386
|
+
|
|
387
|
+
def get_credential_info(self):
|
|
388
|
+
cred = self._current()
|
|
389
|
+
return cred.secret_id, cred.secret_key, cred.token
|
|
390
|
+
|
|
391
|
+
@property
|
|
392
|
+
def secret_id(self):
|
|
393
|
+
return self._current().secret_id
|
|
394
|
+
|
|
395
|
+
@property
|
|
396
|
+
def secretId(self):
|
|
397
|
+
return self.secret_id
|
|
398
|
+
|
|
399
|
+
@property
|
|
400
|
+
def secret_key(self):
|
|
401
|
+
return self._current().secret_key
|
|
402
|
+
|
|
403
|
+
@property
|
|
404
|
+
def secretKey(self):
|
|
405
|
+
return self.secret_key
|
|
406
|
+
|
|
407
|
+
@property
|
|
408
|
+
def token(self):
|
|
409
|
+
return self._current().token
|
|
410
|
+
|
|
411
|
+
@property
|
|
412
|
+
def provider(self):
|
|
413
|
+
return self._provider
|
|
414
|
+
|
|
415
|
+
def __repr__(self): # 不输出 SecretKey / Token
|
|
416
|
+
return "DynamicCredential(product={}, static={})".format(
|
|
417
|
+
self._provider.product_name, self._provider.use_static_secret
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _credential_error(message):
|
|
422
|
+
try:
|
|
423
|
+
from mlflow.exceptions import MlflowException
|
|
424
|
+
|
|
425
|
+
return MlflowException(message)
|
|
426
|
+
except ImportError:
|
|
427
|
+
return ValueError(message)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
_providers = {}
|
|
431
|
+
_providers_lock = threading.Lock()
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def get_default_provider(product_name=PRODUCT_TCCATALOG, project_id=None, region=None,
|
|
435
|
+
auto_refresh=True):
|
|
436
|
+
"""
|
|
437
|
+
获取(并按需创建)进程级共享的 Provider
|
|
438
|
+
|
|
439
|
+
按 (product_name, project_id, region) 维度缓存:wedata 与 tccatalog 的
|
|
440
|
+
token 授权范围不同,不能共用。
|
|
441
|
+
"""
|
|
442
|
+
key = (
|
|
443
|
+
product_name or PRODUCT_TCCATALOG,
|
|
444
|
+
project_id or os.getenv(ENV_PROJECT_ID) or None,
|
|
445
|
+
region,
|
|
446
|
+
)
|
|
447
|
+
with _providers_lock:
|
|
448
|
+
provider = _providers.get(key)
|
|
449
|
+
if provider is None:
|
|
450
|
+
provider = CredentialProvider(
|
|
451
|
+
product_name=key[0], project_id=key[1], region=key[2]
|
|
452
|
+
)
|
|
453
|
+
_providers[key] = provider
|
|
454
|
+
if auto_refresh:
|
|
455
|
+
provider.start_auto_refresh()
|
|
456
|
+
return provider
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def reset_providers():
|
|
460
|
+
"""停止并清空所有 Provider(主要用于测试)"""
|
|
461
|
+
global _providers
|
|
462
|
+
with _providers_lock:
|
|
463
|
+
providers = list(_providers.values())
|
|
464
|
+
_providers = {}
|
|
465
|
+
for provider in providers:
|
|
466
|
+
provider.stop_auto_refresh()
|
|
@@ -6,10 +6,11 @@ import uuid
|
|
|
6
6
|
from datetime import datetime
|
|
7
7
|
|
|
8
8
|
from cachetools import TTLCache
|
|
9
|
-
from tencentcloud.common import credential
|
|
10
9
|
from tencentcloud.common.common_client import CommonClient
|
|
11
10
|
from tencentcloud.common.profile.client_profile import ClientProfile
|
|
12
11
|
|
|
12
|
+
from mlflow_tclake_plugin.credential_provider import PRODUCT_TCCATALOG, get_default_provider
|
|
13
|
+
|
|
13
14
|
from mlflow.entities.model_registry import RegisteredModel, ModelVersion, ModelVersionTag
|
|
14
15
|
from mlflow.exceptions import MlflowException
|
|
15
16
|
from mlflow.protos.databricks_pb2 import INVALID_PARAMETER_VALUE, RESOURCE_ALREADY_EXISTS
|
|
@@ -71,9 +72,6 @@ TCLAKE_MLFLOW_MODEL_SIGNATURE_KEY = "tclake.mlflow.model_signature"
|
|
|
71
72
|
TCLAKE_WEDATA_PROJECT_ID_KEY = "wedata.project"
|
|
72
73
|
TCLAKE_MLFLOW_MODEL_ARTIFACT_PATH_KEY = "tclake.mlflow.artifact_path"
|
|
73
74
|
WEDATA_DATASCIENCE_TYPE = "wedata.datascience.type"
|
|
74
|
-
# 这些内置 key 在写入侧被存成无前缀属性(见 _add_tags_to_properties /
|
|
75
|
-
# _add_project_id_to_properties),读回时需要一并还原成 tag,否则调用方拿不到。
|
|
76
|
-
UNPREFIXED_BUILTIN_TAG_KEYS = (WEDATA_DATASCIENCE_TYPE, TCLAKE_WEDATA_PROJECT_ID_KEY)
|
|
77
75
|
|
|
78
76
|
|
|
79
77
|
def _set_kv_to_properties(key, value, properties=None):
|
|
@@ -219,19 +217,9 @@ def _get_tags_from_properties(properties):
|
|
|
219
217
|
if properties is None:
|
|
220
218
|
return None
|
|
221
219
|
tags = []
|
|
222
|
-
seen_keys = set()
|
|
223
|
-
# 优先取带前缀的属性:调用方通过 tags 参数显式写入的值
|
|
224
220
|
for p in properties:
|
|
225
221
|
if p["Key"].startswith(TCLAKE_MLFLOW_TAG_PREFIX):
|
|
226
|
-
|
|
227
|
-
if tag_key not in seen_keys:
|
|
228
|
-
seen_keys.add(tag_key)
|
|
229
|
-
tags.append(ModelVersionTag(tag_key, p["Value"]))
|
|
230
|
-
# 再补无前缀存储的内置 key,已由带前缀属性覆盖的不重复添加
|
|
231
|
-
for p in properties:
|
|
232
|
-
if p["Key"] in UNPREFIXED_BUILTIN_TAG_KEYS and p["Key"] not in seen_keys:
|
|
233
|
-
seen_keys.add(p["Key"])
|
|
234
|
-
tags.append(ModelVersionTag(p["Key"], p["Value"]))
|
|
222
|
+
tags.append(ModelVersionTag(p["Key"][len(TCLAKE_MLFLOW_TAG_PREFIX):], p["Value"]))
|
|
235
223
|
return tags
|
|
236
224
|
|
|
237
225
|
|
|
@@ -266,8 +254,6 @@ def _get_model_version_name(entity):
|
|
|
266
254
|
def _make_model_version(entity, name):
|
|
267
255
|
properties = entity["Properties"]
|
|
268
256
|
audit = entity["Audit"]
|
|
269
|
-
# 部分接口响应可能不含 Aliases 字段,缺失时按空列表处理
|
|
270
|
-
aliases = entity.get("Aliases") or []
|
|
271
257
|
return ModelVersion(
|
|
272
258
|
name=name,
|
|
273
259
|
version=_get_model_version(entity["Version"]),
|
|
@@ -278,7 +264,6 @@ def _make_model_version(entity, name):
|
|
|
278
264
|
run_id=_get_run_id_from_properties(properties),
|
|
279
265
|
tags=_get_tags_from_properties(properties),
|
|
280
266
|
run_link=_get_run_link_from_properties(properties),
|
|
281
|
-
aliases=aliases,
|
|
282
267
|
status="READY",
|
|
283
268
|
)
|
|
284
269
|
|
|
@@ -318,19 +303,24 @@ class TCLakeStore(AbstractStore):
|
|
|
318
303
|
def __init__(self, store_uri=None, tracking_uri=None):
|
|
319
304
|
super().__init__(store_uri, tracking_uri)
|
|
320
305
|
log_msg("initializing tencent tclake client {} {}".format(store_uri, tracking_uri))
|
|
321
|
-
|
|
322
|
-
if len(sid) == 0:
|
|
323
|
-
raise MlflowException("TENCENTCLOUD_SECRET_ID is not set")
|
|
324
|
-
sk = os.getenv("TENCENTCLOUD_SECRET_KEY", "")
|
|
325
|
-
if len(sk) == 0:
|
|
326
|
-
raise MlflowException("TENCENTCLOUD_SECRET_KEY is not set")
|
|
327
|
-
token = os.getenv("TENCENTCLOUD_TOKEN", None)
|
|
328
|
-
client_profile = get_tencent_cloud_client_profile()
|
|
329
|
-
cred = credential.Credential(sid, sk, token)
|
|
330
|
-
parts = store_uri.split(":")
|
|
306
|
+
parts = (store_uri or "").split(":")
|
|
331
307
|
if len(parts) < 2:
|
|
332
308
|
raise MlflowException("set store_uri tclake:{region}")
|
|
333
309
|
region = parts[1]
|
|
310
|
+
# 凭证优先级:固定密钥 > DescribeCamTemporaryToken 换取的临时密钥。
|
|
311
|
+
# 初始化即启动定时续期(过期前 60s),并包装成动态凭证——
|
|
312
|
+
# SDK 每次请求签名都会重新取值,client 无需重建。
|
|
313
|
+
self.credential_provider = get_default_provider(
|
|
314
|
+
product_name=PRODUCT_TCCATALOG, region=region
|
|
315
|
+
)
|
|
316
|
+
self.credential_provider.start_auto_refresh()
|
|
317
|
+
cloud_credential = self.credential_provider.get()
|
|
318
|
+
if not cloud_credential.secret_id:
|
|
319
|
+
raise MlflowException("TENCENTCLOUD_SECRET_ID is not set")
|
|
320
|
+
if not cloud_credential.secret_key:
|
|
321
|
+
raise MlflowException("TENCENTCLOUD_SECRET_KEY is not set")
|
|
322
|
+
client_profile = get_tencent_cloud_client_profile()
|
|
323
|
+
cred = self.credential_provider.as_sdk_credential()
|
|
334
324
|
self.client = CommonClient("tccatalog", "2024-10-24", cred, region, client_profile)
|
|
335
325
|
self.headers = get_tencent_cloud_headers()
|
|
336
326
|
self.default_catalog_name = os.getenv("TENCENTCLOUD_DEFAULT_CATALOG_NAME", "default")
|
|
@@ -2,7 +2,7 @@ from setuptools import find_packages, setup
|
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name="mlflow-tclake-plugin",
|
|
5
|
-
version="2.1.7.
|
|
5
|
+
version="2.1.7.dev6",
|
|
6
6
|
description="Tclake plugin for MLflow",
|
|
7
7
|
packages=find_packages(),
|
|
8
8
|
# Require MLflow as a dependency of the plugin, so that plugin users can simply install
|
|
File without changes
|
|
File without changes
|
{mlflow_tclake_plugin-2.1.7.dev5 → mlflow_tclake_plugin-2.1.7.dev6}/mlflow_tclake_plugin/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|