dagster-dingtalk 0.1.10b1__py3-none-any.whl → 0.1.11__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- dagster_dingtalk/__init__.py +1 -0
- dagster_dingtalk/app_client.py +198 -19
- dagster_dingtalk/resources.py +259 -67
- dagster_dingtalk/version.py +1 -1
- {dagster_dingtalk-0.1.10b1.dist-info → dagster_dingtalk-0.1.11.dist-info}/METADATA +1 -1
- dagster_dingtalk-0.1.11.dist-info/RECORD +8 -0
- dagster_dingtalk-0.1.10b1.dist-info/RECORD +0 -8
- {dagster_dingtalk-0.1.10b1.dist-info → dagster_dingtalk-0.1.11.dist-info}/WHEEL +0 -0
dagster_dingtalk/__init__.py
CHANGED
@@ -3,6 +3,7 @@ from dagster._core.libraries import DagsterLibraryRegistry
|
|
3
3
|
|
4
4
|
from dagster_dingtalk.resources import DingTalkAppResource
|
5
5
|
from dagster_dingtalk.resources import DingTalkWebhookResource
|
6
|
+
from dagster_dingtalk.app_client import DingTalkClient as DingTalkAppClient
|
6
7
|
# from dagster_dingtalk.operations import DingTalkWebhookOp
|
7
8
|
from dagster_dingtalk.version import __version__
|
8
9
|
|
dagster_dingtalk/app_client.py
CHANGED
@@ -15,48 +15,77 @@ class DingTalkClient:
|
|
15
15
|
self.app_name: str|None = app_name
|
16
16
|
self.agent_id: int|None = agent_id
|
17
17
|
self.client_id: str = client_id
|
18
|
-
self.
|
18
|
+
self.__client_secret: str = client_secret
|
19
19
|
self.robot_code: str = client_id
|
20
20
|
|
21
|
-
access_token: str = self.
|
21
|
+
access_token: str = self.__get_access_token()
|
22
22
|
self.api: Client = Client(base_url="https://api.dingtalk.com/", headers={"x-acs-dingtalk-access-token": access_token})
|
23
23
|
self.oapi: Client = Client(base_url="https://oapi.dingtalk.com/", params={"access_token": access_token})
|
24
24
|
|
25
25
|
self.智能人事 = 智能人事_API(self)
|
26
26
|
self.通讯录管理 = 通讯录管理_API(self)
|
27
27
|
self.文档文件 = 文档文件_API(self)
|
28
|
+
self.互动卡片 = 互动卡片_API(self)
|
28
29
|
|
29
|
-
def
|
30
|
+
def __get_access_token(self) -> str:
|
30
31
|
access_token_cache = Path("/tmp/.dingtalk_cache")
|
32
|
+
all_access_token: dict = {}
|
33
|
+
access_token: str|None = None
|
34
|
+
expire_in: int = 0
|
35
|
+
renew_reason = None
|
31
36
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
37
|
+
# 从缓存中读取
|
38
|
+
if not access_token_cache.exists():
|
39
|
+
renew_reason = f"鉴权缓存不存在"
|
40
|
+
else:
|
41
|
+
try:
|
42
|
+
with open(access_token_cache, 'rb') as f:
|
43
|
+
cache_file = f.read()
|
44
|
+
except Exception as e:
|
45
|
+
logging.error(e)
|
46
|
+
cache_file = None
|
47
|
+
renew_reason = "鉴权缓存读取错误"
|
48
|
+
|
49
|
+
if cache_file:
|
50
|
+
try:
|
51
|
+
all_access_token = pickle.loads(cache_file)
|
52
|
+
except pickle.PickleError:
|
53
|
+
renew_reason = f"鉴权缓存解析错误"
|
54
|
+
|
55
|
+
if all_access_token:
|
56
|
+
app_access_token = all_access_token.get(self.app_id)
|
57
|
+
access_token = app_access_token.get('access_token')
|
58
|
+
expire_in = app_access_token.get('expire_in')
|
59
|
+
else:
|
60
|
+
renew_reason = f"鉴权缓存不存在该应用 {self.app_name}<{self.app_id}>"
|
40
61
|
|
41
|
-
if access_token
|
62
|
+
if not access_token:
|
63
|
+
renew_reason = F"应用 {self.app_name}<{self.app_id}> 的鉴权缓存无效"
|
64
|
+
if expire_in < int(time.time()):
|
65
|
+
renew_reason = F"应用 {self.app_name}<{self.app_id}> 的鉴权缓存过期"
|
66
|
+
|
67
|
+
if renew_reason is None:
|
42
68
|
return access_token
|
43
69
|
else:
|
44
|
-
logging.warning(
|
70
|
+
logging.warning(renew_reason)
|
45
71
|
response = Client().post(
|
46
72
|
url="https://api.dingtalk.com/v1.0/oauth2/accessToken",
|
47
|
-
json={"appKey": self.client_id, "appSecret": self.
|
73
|
+
json={"appKey": self.client_id, "appSecret": self.__client_secret},
|
48
74
|
)
|
49
75
|
access_token:str = response.json().get("accessToken")
|
50
76
|
expire_in:int = response.json().get("expireIn") + int(time.time()) - 60
|
51
77
|
with open(access_token_cache, 'wb') as f:
|
52
|
-
all_access_token[self.app_id] =
|
78
|
+
all_access_token[self.app_id] = {
|
79
|
+
"access_token": access_token,
|
80
|
+
"expire_in": expire_in,
|
81
|
+
}
|
53
82
|
f.write(pickle.dumps(all_access_token))
|
54
83
|
return access_token
|
55
84
|
|
56
|
-
|
57
85
|
# noinspection NonAsciiCharacters
|
58
86
|
class 智能人事_API:
|
59
87
|
def __init__(self, _client:DingTalkClient):
|
88
|
+
self.__client:DingTalkClient = _client
|
60
89
|
self.花名册 = 智能人事_花名册_API(_client)
|
61
90
|
self.员工管理 = 智能人事_员工管理_API(_client)
|
62
91
|
|
@@ -84,7 +113,6 @@ class 智能人事_花名册_API:
|
|
84
113
|
|
85
114
|
# noinspection NonAsciiCharacters
|
86
115
|
class 智能人事_员工管理_API:
|
87
|
-
|
88
116
|
def __init__(self, _client:DingTalkClient):
|
89
117
|
self.__client:DingTalkClient = _client
|
90
118
|
|
@@ -126,7 +154,17 @@ class 智能人事_员工管理_API:
|
|
126
154
|
# noinspection NonAsciiCharacters
|
127
155
|
class 通讯录管理_API:
|
128
156
|
def __init__(self, _client:DingTalkClient):
|
129
|
-
self.__client = _client
|
157
|
+
self.__client:DingTalkClient = _client
|
158
|
+
self.用户管理 = 通讯录管理_用户管理_API(_client)
|
159
|
+
self.部门管理 = 通讯录管理_部门管理_API(_client)
|
160
|
+
|
161
|
+
def 查询用户详情(self, user_id:str, language:str = "zh_CN"):
|
162
|
+
return self.用户管理.查询用户详情(user_id, language)
|
163
|
+
|
164
|
+
# noinspection NonAsciiCharacters
|
165
|
+
class 通讯录管理_用户管理_API:
|
166
|
+
def __init__(self, _client:DingTalkClient):
|
167
|
+
self.__client:DingTalkClient = _client
|
130
168
|
|
131
169
|
def 查询用户详情(self, user_id:str, language:str = "zh_CN") -> dict:
|
132
170
|
response = self.__client.oapi.post(url="/topapi/v2/user/get", json={"language": language, "userid": user_id})
|
@@ -139,17 +177,158 @@ class 通讯录管理_API:
|
|
139
177
|
response = self.__client.api.get(url="/v1.0/contact/empLeaveRecords", params=params)
|
140
178
|
return response.json()
|
141
179
|
|
180
|
+
# noinspection NonAsciiCharacters
|
181
|
+
class 通讯录管理_部门管理_API:
|
182
|
+
def __init__(self, _client:DingTalkClient):
|
183
|
+
self.__client:DingTalkClient = _client
|
184
|
+
|
185
|
+
def 获取部门详情(self, dept_id: int, language:str = "zh_CN") -> dict:
|
186
|
+
"""
|
187
|
+
调用本接口,根据部门ID获取指定部门详情。
|
188
|
+
|
189
|
+
https://open.dingtalk.com/document/orgapp/query-department-details0-v2
|
190
|
+
|
191
|
+
:param dept_id: 部门 ID ,根部门 ID 为 1。
|
192
|
+
:param language: 通讯录语言。zh_CN en_US
|
193
|
+
"""
|
194
|
+
response = self.__client.oapi.post(
|
195
|
+
url="/topapi/v2/department/get",
|
196
|
+
json={"language": language, "dept_id": dept_id}
|
197
|
+
)
|
198
|
+
return response.json()
|
199
|
+
|
200
|
+
def 获取部门列表(self, dept_id: int, language:str = "zh_CN"):
|
201
|
+
"""
|
202
|
+
调用本接口,获取下一级部门基础信息。
|
203
|
+
|
204
|
+
https://open.dingtalk.com/document/orgapp/obtain-the-department-list-v2
|
205
|
+
|
206
|
+
:param dept_id: 部门 ID ,根部门 ID 为 1。
|
207
|
+
:param language: 通讯录语言。zh_CN en_US
|
208
|
+
"""
|
209
|
+
response = self.__client.oapi.post(
|
210
|
+
url="/topapi/v2/department/listsub",
|
211
|
+
json={"language": language, "dept_id": dept_id}
|
212
|
+
)
|
213
|
+
return response.json()
|
214
|
+
|
142
215
|
# noinspection NonAsciiCharacters
|
143
216
|
class 文档文件_API:
|
144
217
|
def __init__(self, _client:DingTalkClient):
|
218
|
+
self.__client:DingTalkClient = _client
|
145
219
|
self.媒体文件 = 文档文件_媒体文件_API(_client)
|
146
220
|
|
147
221
|
# noinspection NonAsciiCharacters
|
148
222
|
class 文档文件_媒体文件_API:
|
149
223
|
def __init__(self, _client:DingTalkClient):
|
150
|
-
self.__client = _client
|
224
|
+
self.__client:DingTalkClient = _client
|
151
225
|
|
152
226
|
def 上传媒体文件(self, file_path:Path|str, media_type:Literal['image', 'voice', 'video', 'file']) -> dict:
|
227
|
+
"""
|
228
|
+
调用本接口,上传图片、语音媒体资源文件以及普通文件,接口返回媒体资源标识 media_id。
|
229
|
+
|
230
|
+
https://open.dingtalk.com/document/orgapp/upload-media-files
|
231
|
+
|
232
|
+
:param file_path: 本地文件路径
|
233
|
+
:param media_type: 媒体类型,支持 'image', 'voice', 'video', 'file'
|
234
|
+
|
235
|
+
:return:
|
236
|
+
{
|
237
|
+
"errcode": 0,
|
238
|
+
"errmsg": "ok",
|
239
|
+
"media_id": "$iAEKAqDBgTNAk",
|
240
|
+
"created_at": 1605863153573,
|
241
|
+
"type": "image"
|
242
|
+
}
|
243
|
+
"""
|
153
244
|
with open(file_path, 'rb') as f:
|
154
245
|
response = self.__client.oapi.post(url=f"/media/upload?type={media_type}", files={'media': f})
|
155
246
|
return response.json()
|
247
|
+
|
248
|
+
# noinspection NonAsciiCharacters
|
249
|
+
class 互动卡片_API:
|
250
|
+
def __init__(self, _client:DingTalkClient):
|
251
|
+
self.__client:DingTalkClient = _client
|
252
|
+
|
253
|
+
def 创建并投放卡片(
|
254
|
+
self, search_type_name: str, search_desc: str, card_template_id: str, card_param_map: dict,
|
255
|
+
alert_content: str, open_space_ids: List[str], out_track_id: str, support_forward: bool = True,
|
256
|
+
call_back_type: str = "STREAM", expired_time_millis:int = 0
|
257
|
+
) -> dict:
|
258
|
+
"""
|
259
|
+
创建并投放卡片。当前仅支持 IM群聊, IM机器人单聊, 吊顶 三种场域类型。
|
260
|
+
|
261
|
+
https://open.dingtalk.com/document/orgapp/create-and-deliver-cards
|
262
|
+
|
263
|
+
:param card_template_id: 卡片模板 ID
|
264
|
+
:param open_space_ids: 卡片投放场域 Id
|
265
|
+
:param out_track_id: 卡片唯一标识
|
266
|
+
:param card_param_map: 卡片数据
|
267
|
+
:param search_type_name: 卡片类型名
|
268
|
+
:param search_desc: 卡片消息展示
|
269
|
+
:param alert_content: 通知内容
|
270
|
+
:param call_back_type: 回调模式
|
271
|
+
:param support_forward: 是否支持转发
|
272
|
+
:param expired_time_millis: 吊顶投放过期时间。当投放内容为吊顶时必须传参。
|
273
|
+
"""
|
274
|
+
|
275
|
+
open_space_id = f"dtv1.card//{';'.join(open_space_ids)}"
|
276
|
+
|
277
|
+
payload = {
|
278
|
+
"cardTemplateId": card_template_id,
|
279
|
+
"outTrackId": out_track_id,
|
280
|
+
"openSpaceId": open_space_id,
|
281
|
+
"callbackType": call_back_type,
|
282
|
+
"cardData": {"cardParamMap": card_param_map}
|
283
|
+
}
|
284
|
+
|
285
|
+
open_space_model = {
|
286
|
+
"supportForward": support_forward,
|
287
|
+
"searchSupport": {"searchTypeName": search_type_name, "searchDesc": search_desc},
|
288
|
+
"notification": {"alertContent": alert_content, "notificationOff": False}
|
289
|
+
}
|
290
|
+
|
291
|
+
if 'IM_GROUP' in open_space_id.upper():
|
292
|
+
payload["imGroupOpenSpaceModel"] = open_space_model
|
293
|
+
payload["imGroupOpenDeliverModel"] = {"robotCode": self.__client.robot_code}
|
294
|
+
|
295
|
+
if 'IM_ROBOT' in open_space_id.upper():
|
296
|
+
payload["imRobotOpenSpaceModel"] = open_space_model
|
297
|
+
payload["imRobotOpenDeliverModel"] = {"spaceType": "IM_ROBOT", "robotCode": self.__client.robot_code}
|
298
|
+
|
299
|
+
if 'ONE_BOX' in open_space_id.upper():
|
300
|
+
if expired_time_millis == 0:
|
301
|
+
expired_time_millis = int(time.time()+3600)*1000
|
302
|
+
payload["topOpenSpaceModel"] = {"spaceType": "ONE_BOX"}
|
303
|
+
payload["topOpenDeliverModel"] = {"platforms": ["android","ios","win","mac"], "expiredTimeMillis": expired_time_millis,}
|
304
|
+
|
305
|
+
response = self.__client.api.post(
|
306
|
+
url="/v1.0/card/instances/createAndDeliver",
|
307
|
+
json=payload
|
308
|
+
)
|
309
|
+
|
310
|
+
return response.json()
|
311
|
+
|
312
|
+
def 更新卡片(self, out_track_id: str, card_param_map: dict, update_card_data_by_key:bool=True) -> dict:
|
313
|
+
"""
|
314
|
+
调用本接口,实现主动更新卡片数据。
|
315
|
+
|
316
|
+
https://open.dingtalk.com/document/orgapp/interactive-card-update-interface
|
317
|
+
|
318
|
+
:param out_track_id: 外部卡片实例Id。
|
319
|
+
:param card_param_map: 卡片模板内容。
|
320
|
+
:param update_card_data_by_key: True-按 key 更新 cardData 数据 False-覆盖更新 cardData 数据
|
321
|
+
:return:
|
322
|
+
{success: bool, result: bool}
|
323
|
+
"""
|
324
|
+
|
325
|
+
response = self.__client.api.put(
|
326
|
+
url="/v1.0/card/instances",
|
327
|
+
json={
|
328
|
+
"outTrackId": out_track_id,
|
329
|
+
"cardData": {"cardParamMap": card_param_map},
|
330
|
+
"cardUpdateOptions": {"updateCardDataByKey": update_card_data_by_key}
|
331
|
+
}
|
332
|
+
)
|
333
|
+
|
334
|
+
return response.json()
|
dagster_dingtalk/resources.py
CHANGED
@@ -7,27 +7,123 @@ import urllib.parse
|
|
7
7
|
from typing import Optional, Tuple, List, Literal
|
8
8
|
import httpx
|
9
9
|
from pydantic import Field
|
10
|
-
from app_client import DingTalkClient
|
10
|
+
from .app_client import DingTalkClient
|
11
11
|
from dagster import ConfigurableResource, InitResourceContext
|
12
12
|
|
13
13
|
|
14
|
+
DINGTALK_WEBHOOK_EXCEPTION_SOLUTIONS = {
|
15
|
+
"-1": "\n系统繁忙,请稍后重试",
|
16
|
+
"40035": "\n缺少参数 json,请补充消息json",
|
17
|
+
"43004": "\n无效的HTTP HEADER Content-Type,请设置具体的消息参数",
|
18
|
+
"400013": "\n群已被解散,请向其他群发消息",
|
19
|
+
"400101": "\naccess_token不存在,请确认access_token拼写是否正确",
|
20
|
+
"400102": "\n机器人已停用,请联系管理员启用机器人",
|
21
|
+
"400105": "\n不支持的消息类型,请使用文档中支持的消息类型",
|
22
|
+
"400106": "\n机器人不存在,请确认机器人是否在群中",
|
23
|
+
"410100": "\n发送速度太快而限流,请降低发送速度",
|
24
|
+
"430101": "\n含有不安全的外链,请确认发送的内容合法",
|
25
|
+
"430102": "\n含有不合适的文本,请确认发送的内容合法",
|
26
|
+
"430103": "\n含有不合适的图片,请确认发送的内容合法",
|
27
|
+
"430104": "\n含有不合适的内容,请确认发送的内容合法",
|
28
|
+
"310000": "\n消息校验未通过,请查看机器人的安全设置",
|
29
|
+
}
|
30
|
+
|
31
|
+
|
32
|
+
class DingTalkWebhookException(Exception):
|
33
|
+
def __init__(self, errcode, errmsg):
|
34
|
+
self.errcode = errcode
|
35
|
+
self.errmsg = errmsg
|
36
|
+
super().__init__(f"DingTalkWebhookError {errcode}: {errmsg} {DINGTALK_WEBHOOK_EXCEPTION_SOLUTIONS.get(errcode)}")
|
37
|
+
|
38
|
+
|
14
39
|
class DingTalkWebhookResource(ConfigurableResource):
|
15
40
|
"""
|
16
|
-
|
41
|
+
该资源允许定义单个钉钉自定义机器人的 Webhook 端点,以便于发送文本、Markdown、Link、 ActionCard、FeedCard 消息,消息具体样式可参考
|
42
|
+
[钉钉开放平台 | 自定义机器人发送消息的消息类型](https://open.dingtalk.com/document/orgapp/custom-bot-send-message-type)。
|
43
|
+
|
44
|
+
### 配置项:
|
45
|
+
|
46
|
+
- **access_token** (str):
|
47
|
+
机器人 Webhook 地址中的 access_token 值。
|
48
|
+
- **secret** (str, optional):
|
49
|
+
如使用加签安全配置,则需传签名密钥。默认值为 None。
|
50
|
+
- **alias** (str, optional):
|
51
|
+
如提供别名,可以在使用 `MultiDingTalkWebhookResource` 中使用别名进行 webhook 选择。默认值为 None。
|
52
|
+
- **base_url** (str, optional):
|
53
|
+
通用地址,一般无需更改。默认值为 “https://oapi.dingtalk.com/robot/send”。
|
54
|
+
|
55
|
+
### 用例:
|
56
|
+
|
57
|
+
1. 使用单个资源:
|
58
|
+
|
59
|
+
```python
|
60
|
+
from dagster_dingtalk import DingTalkWebhookResource
|
61
|
+
|
62
|
+
@op(required_resource_keys={"dingtalk_webhook"}, ins={"text": In(str)})
|
63
|
+
def op_send_text(context:OpExecutionContext, text:str):
|
64
|
+
dingtalk_webhook:DingTalkWebhookResource = context.resources.dingtalk_webhook
|
65
|
+
result = dingtalk.send_text(text)
|
66
|
+
|
67
|
+
@job
|
68
|
+
def job_send_text():
|
69
|
+
op_send_text
|
70
|
+
|
71
|
+
defs = Definitions(
|
72
|
+
jobs=job_user_info,
|
73
|
+
resources={"dingtalk_webhook": DingTalkWebhookResource(access_token = "<access_token>", secret = "<secret>")}
|
74
|
+
)
|
75
|
+
```
|
76
|
+
|
77
|
+
2. 启动时动态构建企业内部应用资源, 可参考 [Dagster文档 | 在启动时配置资源](https://docs.dagster.io/concepts/resources#configuring-resources-at-launch-time)
|
78
|
+
|
79
|
+
```python
|
80
|
+
from dagster_dingtalk import DingTalkWebhookResource
|
17
81
|
|
18
|
-
|
19
|
-
|
82
|
+
@op(required_resource_keys={"dingtalk_webhook"}, ins={"text": In(str)})
|
83
|
+
def op_send_text(context:OpExecutionContext, text:str):
|
84
|
+
dingtalk_webhook:DingTalkWebhookResource = context.resources.dingtalk_webhook
|
85
|
+
result = dingtalk.send_text(text)
|
86
|
+
|
87
|
+
@job
|
88
|
+
def job_send_text():
|
89
|
+
op_send_text
|
90
|
+
|
91
|
+
dingtalk_webhooks = {
|
92
|
+
"Group1" : DingTalkWebhookResource(access_token="<access_token>", secret="<secret>", alias="Group1"),
|
93
|
+
"Group2" : DingTalkWebhookResource(access_token="<access_token>", secret="<secret>", alias="Group2")
|
94
|
+
}
|
95
|
+
|
96
|
+
defs = Definitions(jobs=job_send_text, resources={"dingtalk_webhook": DingTalkWebhookResource.configure_at_launch()})
|
97
|
+
|
98
|
+
@schedule(cron_schedule="20 9 * * *", job=job_send_text)
|
99
|
+
def schedule_user_info():
|
100
|
+
return RunRequest(run_config=RunConfig(
|
101
|
+
ops={"op_send_text": {"inputs": {"text": "This a test text."}}},
|
102
|
+
resources={"dingtalk": dingtalk_webhooks["Group1"]},
|
103
|
+
))
|
104
|
+
```
|
105
|
+
|
106
|
+
### 注意:
|
107
|
+
|
108
|
+
应该永远避免直接将密钥字符串直接配置给资源,这会导致在 dagster 前端用户界面暴露密钥。
|
109
|
+
应当从环境变量中读取密钥。你可以在代码中注册临时的环境变量,或从系统中引入环境变量。
|
110
|
+
|
111
|
+
```python
|
112
|
+
import os
|
113
|
+
from dagster_dingtalk import DingTalkWebhookResource
|
114
|
+
|
115
|
+
# 直接在代码中注册临时的环境变量
|
116
|
+
os.environ.update({'access_token': "<access_token>"})
|
117
|
+
os.environ.update({'secret': "<secret>"})
|
118
|
+
|
119
|
+
webhook = DingTalkWebhookResource(access_token=EnvVar("access_token"), secret=EnvVar("secret"))
|
20
120
|
|
21
|
-
Args:
|
22
|
-
access_token (str): 机器人 Webhook 地址中的 access_token 值。
|
23
|
-
secret (str, optional): 如使用加签安全配置,则需传签名密钥。默认值为 None。
|
24
|
-
alias (str, optional): 如提供别名,可以在使用 `MultiDingTalkWebhookResource` 中使用别名进行 webhook 选择。默认值为 None。
|
25
|
-
base_url (str, optional): 通用地址,一般无需更改。默认值为 “https://oapi.dingtalk.com/robot/send”。
|
26
121
|
|
27
122
|
"""
|
123
|
+
|
28
124
|
access_token: str = Field(description="Webhook地址中的 access_token 部分")
|
29
125
|
secret: Optional[str] = Field(default=None, description="如使用加签安全配置,需传签名密钥")
|
30
|
-
alias: Optional[str] = Field(default=None, description="
|
126
|
+
alias: Optional[str] = Field(default=None, description="别名,标记用,无实际意义")
|
31
127
|
base_url: str = Field(default="https://oapi.dingtalk.com/robot/send", description="Webhook的通用地址,无需更改")
|
32
128
|
|
33
129
|
def webhook_url(self):
|
@@ -37,7 +133,7 @@ class DingTalkWebhookResource(ConfigurableResource):
|
|
37
133
|
钉钉API文档:
|
38
134
|
https://open.dingtalk.com/document/robots/custom-robot-access
|
39
135
|
|
40
|
-
|
136
|
+
:return:
|
41
137
|
str: Webhook URL
|
42
138
|
"""
|
43
139
|
if self.secret is None:
|
@@ -55,14 +151,28 @@ class DingTalkWebhookResource(ConfigurableResource):
|
|
55
151
|
"""
|
56
152
|
从文本截取前12个字符作为标题,并清理其中的 Markdown 格式字符。
|
57
153
|
|
58
|
-
|
59
|
-
text: 原文
|
154
|
+
:param text: 原文
|
60
155
|
|
61
|
-
|
156
|
+
:return:
|
62
157
|
str: 标题
|
63
158
|
"""
|
64
159
|
return re.sub(r'[\n#>* ]', '', text[:12])
|
65
160
|
|
161
|
+
@staticmethod
|
162
|
+
def __handle_response(response):
|
163
|
+
"""
|
164
|
+
处理钉钉 Webhook API 响应,根据 errcode 抛出相应的异常
|
165
|
+
|
166
|
+
:param response: 钉钉Webhook API响应的JSON数据
|
167
|
+
"""
|
168
|
+
errcode = response.json().get("errcode")
|
169
|
+
errmsg = response.json().get("errmsg")
|
170
|
+
|
171
|
+
if errcode == "0":
|
172
|
+
return True
|
173
|
+
else:
|
174
|
+
raise DingTalkWebhookException(errcode, errmsg)
|
175
|
+
|
66
176
|
def send_text(self, text: str,
|
67
177
|
at_mobiles:List[str]|None = None, at_user_ids:List[str]|None = None, at_all:bool = False):
|
68
178
|
"""
|
@@ -71,18 +181,21 @@ class DingTalkWebhookResource(ConfigurableResource):
|
|
71
181
|
钉钉API文档:
|
72
182
|
https://open.dingtalk.com/document/orgapp/custom-bot-send-message-type
|
73
183
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
184
|
+
:param str text: 待发送文本
|
185
|
+
:param List[str],optional at_mobiles: 需要 @ 的用户手机号。默认值为 None
|
186
|
+
:param List[str],optional at_user_ids: 需要 @ 的用户 UserID。默认值为 None
|
187
|
+
:param bool,optional at_all: 是否 @ 所有人。默认值为 False
|
188
|
+
|
189
|
+
:raise DingTalkWebhookException:
|
190
|
+
|
79
191
|
"""
|
80
192
|
at = {"isAtAll": at_all}
|
81
193
|
if at_user_ids:
|
82
194
|
at["atUserIds"] = at_user_ids
|
83
195
|
if at_mobiles:
|
84
196
|
at["atMobiles"] = at_mobiles
|
85
|
-
httpx.post(url=self.webhook_url(), json={"msgtype": "text", "text": {"content": text}, "at": at})
|
197
|
+
response = httpx.post(url=self.webhook_url(), json={"msgtype": "text", "text": {"content": text}, "at": at})
|
198
|
+
self.__handle_response(response)
|
86
199
|
|
87
200
|
def send_link(self, text: str, message_url:str, title:str|None = None, pic_url:str = ""):
|
88
201
|
"""
|
@@ -91,31 +204,26 @@ class DingTalkWebhookResource(ConfigurableResource):
|
|
91
204
|
钉钉API文档:
|
92
205
|
https://open.dingtalk.com/document/orgapp/custom-bot-send-message-type
|
93
206
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
207
|
+
:param str text: 待发送文本
|
208
|
+
:param str message_url: 链接的 Url
|
209
|
+
:param str,optional title: 标题,在通知和被引用时显示的简短信息。默认从文本中生成。
|
210
|
+
:param str,optional pic_url: 图片的 Url,默认为 None
|
211
|
+
|
212
|
+
:raise DingTalkWebhookException:
|
99
213
|
"""
|
100
214
|
title = title or self._gen_title(text)
|
101
|
-
httpx.post(
|
215
|
+
response = httpx.post(
|
102
216
|
url=self.webhook_url(),
|
103
217
|
json={"msgtype": "link", "link": {"title": title, "text": text, "picUrl": pic_url, "messageUrl": message_url}}
|
104
218
|
)
|
219
|
+
self.__handle_response(response)
|
105
220
|
|
106
221
|
def send_markdown(self, text: List[str]|str, title:str|None = None,
|
107
222
|
at_mobiles:List[str]|None = None, at_user_ids:List[str]|None = None, at_all:bool = False):
|
108
223
|
"""
|
109
224
|
发送 Markdown 消息。支持的语法有:
|
110
|
-
# 一级标题
|
111
|
-
|
112
|
-
### 三级标题
|
113
|
-
#### 四级标题
|
114
|
-
##### 五级标题
|
115
|
-
###### 六级标题
|
116
|
-
> 引用
|
117
|
-
**加粗**
|
118
|
-
*斜体*
|
225
|
+
# 一级标题 ## 二级标题 ### 三级标题 #### 四级标题 ##### 五级标题 ###### 六级标题
|
226
|
+
> 引用 **加粗** *斜体*
|
119
227
|
[链接跳转](https://example.com/doc.html)
|
120
228
|
![图片预览](https://example.com/pic.jpg)
|
121
229
|
- 无序列表
|
@@ -124,12 +232,13 @@ class DingTalkWebhookResource(ConfigurableResource):
|
|
124
232
|
钉钉API文档:
|
125
233
|
https://open.dingtalk.com/document/orgapp/custom-bot-send-message-type
|
126
234
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
235
|
+
:param str text: 待发送文本
|
236
|
+
:param str,optional title: 标题,在通知和被引用时显示的简短信息。默认从文本中生成。
|
237
|
+
:param List[str],optional at_mobiles: 需要 @ 的用户手机号。默认值为 None
|
238
|
+
:param List[str],optional at_user_ids: 需要 @ 的用户 UserID。默认值为 None
|
239
|
+
:param bool,optional at_all: 是否 @ 所有人。默认值为 False
|
240
|
+
|
241
|
+
:raise DingTalkWebhookException:
|
133
242
|
"""
|
134
243
|
text = text if isinstance(text, str) else "\n\n".join(text)
|
135
244
|
title = title or self._gen_title(text)
|
@@ -138,43 +247,44 @@ class DingTalkWebhookResource(ConfigurableResource):
|
|
138
247
|
at["atUserIds"] = at_user_ids
|
139
248
|
if at_mobiles:
|
140
249
|
at["atMobiles"] = at_mobiles
|
141
|
-
httpx.post(url=self.webhook_url(),json={"msgtype": "markdown", "markdown": {"title": title, "text": text}, "at": at})
|
250
|
+
response = httpx.post(url=self.webhook_url(),json={"msgtype": "markdown", "markdown": {"title": title, "text": text}, "at": at})
|
251
|
+
self.__handle_response(response)
|
142
252
|
|
143
253
|
def send_action_card(self, text: List[str]|str, title:str|None = None, btn_orientation:Literal["0","1"] = "0",
|
144
254
|
single_jump:Tuple[str,str]|None = None, btns_jump:List[Tuple[str,str]]|None = None):
|
145
255
|
"""
|
146
256
|
发送跳转 ActionCard 消息。
|
147
257
|
|
148
|
-
|
149
|
-
|
150
|
-
title (str, optional): 标题,在通知和被引用时显示的简短信息。默认从文本中生成。
|
151
|
-
btn_orientation (str, optional): 按钮排列方式,0-按钮竖直排列,1-按钮横向排列。默认值为 "0"
|
152
|
-
single_jump(Tuple[str,str], optional): 传此参数为单个按钮,元组内第一项为按钮的标题,第二项为按钮链接。
|
153
|
-
btns_jump(Tuple[str,str], optional): 传此参数为多个按钮,元组内第一项为按钮的标题,第二项为按钮链接。
|
258
|
+
**注意:**
|
259
|
+
同时传 `single_jump` 和 `btns_jump`,仅 `single_jump` 生效。
|
154
260
|
|
155
|
-
|
156
|
-
|
261
|
+
:param str text: 待发送文本,支持 Markdown 部分语法。
|
262
|
+
:param str,optional title: 标题,在通知和被引用时显示的简短信息。默认从文本中生成。
|
263
|
+
:param str,optional btn_orientation: 按钮排列方式,0-按钮竖直排列,1-按钮横向排列。默认值为 "0"
|
264
|
+
:param Tuple[str,str],optional single_jump: 传此参数为单个按钮,元组内第一项为按钮的标题,第二项为按钮链接。
|
265
|
+
:param Tuple[str,str],optional btns_jump: 传此参数为多个按钮,元组内第一项为按钮的标题,第二项为按钮链接。
|
157
266
|
|
267
|
+
:raise DingTalkWebhookException:
|
158
268
|
"""
|
159
269
|
text = text if isinstance(text, str) else "\n\n".join(text)
|
160
270
|
title = title or self._gen_title(text)
|
161
271
|
action_card = {"title": title, "text": text, "btnOrientation": str(btn_orientation)}
|
272
|
+
|
162
273
|
if single_jump:
|
163
274
|
action_card["singleTitle"], action_card["singleURL"] = single_jump
|
164
|
-
|
165
|
-
elif btns_jump:
|
275
|
+
if btns_jump:
|
166
276
|
action_card["btns"] = [{"title": action_title, "actionURL": action_url} for action_title, action_url in btns_jump]
|
167
|
-
|
168
|
-
|
169
|
-
|
277
|
+
|
278
|
+
response = httpx.post(url=self.webhook_url(), json={"msgtype": "actionCard", "actionCard": action_card})
|
279
|
+
self.__handle_response(response)
|
170
280
|
|
171
281
|
def send_feed_card(self, *args:Tuple[str,str,str]):
|
172
282
|
"""
|
173
283
|
发送 FeedCard 消息。
|
174
284
|
|
175
|
-
|
176
|
-
args (Tuple[str,str,str]): 可以传入任意个具有三个元素的元组,分别为 (标题, 跳转链接, 缩略图链接)
|
285
|
+
:param Tuple[str,str,str],optional args: 可以传入任意个具有三个元素的元组,分别为 `(标题, 跳转链接, 缩略图链接)`
|
177
286
|
|
287
|
+
:raise DingTalkWebhookException:
|
178
288
|
"""
|
179
289
|
for a in args:
|
180
290
|
print(a)
|
@@ -182,23 +292,100 @@ class DingTalkWebhookResource(ConfigurableResource):
|
|
182
292
|
{"title": title, "messageURL": message_url, "picURL": pic_url}
|
183
293
|
for title, message_url, pic_url in args
|
184
294
|
]
|
185
|
-
httpx.post(url=self.webhook_url(), json={"msgtype": "feedCard", "feedCard": {"links": links_data}})
|
295
|
+
response = httpx.post(url=self.webhook_url(), json={"msgtype": "feedCard", "feedCard": {"links": links_data}})
|
296
|
+
self.__handle_response(response)
|
186
297
|
|
187
298
|
|
188
299
|
class DingTalkAppResource(ConfigurableResource):
|
189
300
|
"""
|
190
301
|
[钉钉服务端 API](https://open.dingtalk.com/document/orgapp/api-overview) 企业内部应用部分的第三方封装。
|
191
|
-
通过此资源,可以调用部分钉钉服务端API。
|
192
302
|
|
193
|
-
|
194
|
-
|
303
|
+
通过此资源,可以调用部分钉钉服务端 API。具体封装的 API 可以在 IDE 中通过引入 `DingTalkAppClient` 类来查看 IDE 提示:
|
304
|
+
|
305
|
+
`from dagster_dingtalk import DingTalkAppClient`
|
306
|
+
|
307
|
+
### 配置项:
|
308
|
+
|
309
|
+
- **AppID** (str):
|
310
|
+
应用应用唯一标识 AppID,作为缓存标识符使用。不传入则不缓存鉴权。
|
311
|
+
- **AgentID** (int, optional):
|
312
|
+
原企业内部应用 AgentId ,部分 API 会使用到。默认值为 None
|
313
|
+
- **AppName** (str, optional):
|
314
|
+
应用名。
|
315
|
+
- **ClientId** (str):
|
316
|
+
应用的 Client ID ,原 AppKey 和 SuiteKey
|
317
|
+
- **ClientSecret** (str):
|
318
|
+
应用的 Client Secret ,原 AppSecret 和 SuiteSecret
|
319
|
+
|
320
|
+
### 用例:
|
321
|
+
|
322
|
+
1. 使用单一的企业内部应用资源。
|
323
|
+
|
324
|
+
```python
|
325
|
+
from dagster_dingtalk import DingTalkAppResource
|
326
|
+
|
327
|
+
@op(required_resource_keys={"dingtalk"}, ins={"user_id": In(str)})
|
328
|
+
def op_user_info(context:OpExecutionContext, user_id:str):
|
329
|
+
dingtalk:DingTalkAppClient = context.resources.dingtalk
|
330
|
+
result = dingtalk.通讯录管理.用户管理.查询用户详情(user_id).get('result')
|
331
|
+
context.log.info(result)
|
332
|
+
|
333
|
+
@job
|
334
|
+
def job_user_info():
|
335
|
+
op_user_info
|
336
|
+
|
337
|
+
defs = Definitions(jobs=job_user_info, resources={
|
338
|
+
"dingtalk": DingTalkAppResource(
|
339
|
+
AppID = "<the-app-id>",
|
340
|
+
ClientId = "<the-client-id>",
|
341
|
+
ClientSecret = EnvVar("<the-client-secret-env-name>"),
|
342
|
+
)
|
343
|
+
})
|
344
|
+
```
|
345
|
+
|
346
|
+
2. 启动时动态构建企业内部应用资源, 可参考 [Dagster文档 | 在启动时配置资源](https://docs.dagster.io/concepts/resources#configuring-resources-at-launch-time)
|
347
|
+
|
348
|
+
```python
|
349
|
+
from dagster_dingtalk import DingTalkAppResource
|
350
|
+
|
351
|
+
@op(required_resource_keys={"dingtalk"}, ins={"user_id": In(str)})
|
352
|
+
def op_user_info(context:OpExecutionContext, user_id:str):
|
353
|
+
dingtalk:DingTalkAppClient = context.resources.dingtalk
|
354
|
+
result = dingtalk.通讯录管理.用户管理.查询用户详情(user_id).get('result')
|
355
|
+
context.log.info(result)
|
356
|
+
|
357
|
+
@job
|
358
|
+
def job_user_info():
|
359
|
+
op_user_info()
|
360
|
+
|
361
|
+
dingtalk_apps = {
|
362
|
+
"App1" : DingTalkAppResource(
|
363
|
+
AppID = "<app-1-app-id>",
|
364
|
+
ClientId = "<app-1-client-id>",
|
365
|
+
ClientSecret = EnvVar("<app-1-client-secret-env-name>"),
|
366
|
+
),
|
367
|
+
"App2" : DingTalkAppResource(
|
368
|
+
AppID = "<app-2-app-id>",
|
369
|
+
ClientId = "<app-2-client-id>",
|
370
|
+
ClientSecret = EnvVar("<app-2-client-secret-env-name>"),
|
371
|
+
)
|
372
|
+
}
|
373
|
+
|
374
|
+
defs = Definitions(jobs=job_user_info, resources={"dingtalk": DingTalkAppResource.configure_at_launch()})
|
375
|
+
|
376
|
+
@schedule(cron_schedule="20 9 * * *", job=job_user_info)
|
377
|
+
def schedule_user_info():
|
378
|
+
return RunRequest(run_config=RunConfig(
|
379
|
+
ops={"op_user_info": {"inputs": {"user_id": "<the-user-id>"}}},
|
380
|
+
resources={"dingtalk": dingtalk_apps["App1"]},
|
381
|
+
))
|
382
|
+
```
|
383
|
+
|
384
|
+
### 注意:
|
385
|
+
|
386
|
+
应该永远避免直接将密钥字符串直接配置给资源,这会导致在 dagster 前端用户界面暴露密钥。
|
387
|
+
应当从环境变量中读取密钥。你可以在代码中注册临时的环境变量,或从系统中引入环境变量。
|
195
388
|
|
196
|
-
Args:
|
197
|
-
AppID (str): 应用应用唯一标识 AppID,作为缓存标识符使用。不传入则不缓存鉴权。
|
198
|
-
AgentID (int, optional): 原企业内部应用 AgentId ,部分 API 会使用到。默认值为 None
|
199
|
-
AppName (str, optional): 应用名。
|
200
|
-
ClientId (str): 应用的 Client ID ,原 AppKey 和 SuiteKey
|
201
|
-
ClientSecret (str): 应用的 Client Secret ,原 AppSecret 和 SuiteSecret
|
202
389
|
"""
|
203
390
|
|
204
391
|
AppID: str = Field(description="应用应用唯一标识 AppID,作为缓存标识符使用。不传入则不缓存鉴权。")
|
@@ -212,6 +399,11 @@ class DingTalkAppResource(ConfigurableResource):
|
|
212
399
|
return False
|
213
400
|
|
214
401
|
def create_resource(self, context: InitResourceContext) -> DingTalkClient:
|
402
|
+
"""
|
403
|
+
返回一个 `DingTalkClient` 实例。
|
404
|
+
:param context:
|
405
|
+
:return:
|
406
|
+
"""
|
215
407
|
return DingTalkClient(
|
216
408
|
app_id=self.AppID,
|
217
409
|
agent_id=self.AgentID,
|
dagster_dingtalk/version.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "0.1.
|
1
|
+
__version__ = "0.1.11"
|
@@ -0,0 +1,8 @@
|
|
1
|
+
dagster_dingtalk/__init__.py,sha256=X7r8JoydXOsT9Sis4rBpVSKQeKJnnZ_t_qFae-ASF7E,466
|
2
|
+
dagster_dingtalk/app_client.py,sha256=xZ9NcVZuWvz7S2thm4B1h4aFiBXmUnkp5nK5N_k7HwE,14011
|
3
|
+
dagster_dingtalk/operations.py,sha256=3cCZCxh-dAdzzb75WCTKVdzeMV8yu_JJpIeULS7XaNg,761
|
4
|
+
dagster_dingtalk/resources.py,sha256=XXGRub47EZY2MfAsGhyEZ_Joh_D-BMFNh_jXCVPx6tU,17255
|
5
|
+
dagster_dingtalk/version.py,sha256=nllDrH0jyChMuuYrK0CC55iTBKUNTUjejtcwxyUF2EQ,23
|
6
|
+
dagster_dingtalk-0.1.11.dist-info/METADATA,sha256=AqQfS-lsnBW-PBoad9xIsZei9b995Yf4SJhaHq1YQ7c,1221
|
7
|
+
dagster_dingtalk-0.1.11.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
8
|
+
dagster_dingtalk-0.1.11.dist-info/RECORD,,
|
@@ -1,8 +0,0 @@
|
|
1
|
-
dagster_dingtalk/__init__.py,sha256=TUfdRP1n-tifjBuC9ugDxzeNu-BtjmX2fEFDgTitHeo,390
|
2
|
-
dagster_dingtalk/app_client.py,sha256=nti06ZM_LKvdDkHryt1-beIKZrYcOkUKDznXXnqn8qg,6747
|
3
|
-
dagster_dingtalk/operations.py,sha256=3cCZCxh-dAdzzb75WCTKVdzeMV8yu_JJpIeULS7XaNg,761
|
4
|
-
dagster_dingtalk/resources.py,sha256=SuQiNvq50eqdGT3bT0eiQ7whv6CGp8rZsqfHL6gBtf4,9780
|
5
|
-
dagster_dingtalk/version.py,sha256=Nw9m62L0Jey5qchmcQFQeIRAFUlcjZewmyhYlfs0pG0,25
|
6
|
-
dagster_dingtalk-0.1.10b1.dist-info/METADATA,sha256=m6GrwJ60HSHFP4UfVSO6y5ybcIwFs_8TXNowkjjKRnw,1223
|
7
|
-
dagster_dingtalk-0.1.10b1.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
8
|
-
dagster_dingtalk-0.1.10b1.dist-info/RECORD,,
|
File without changes
|