dagster-dingtalk 0.1.10b2__py3-none-any.whl → 0.1.11__py3-none-any.whl
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.
- dagster_dingtalk/__init__.py +1 -0
- dagster_dingtalk/app_client.py +198 -19
- dagster_dingtalk/resources.py +258 -66
- dagster_dingtalk/version.py +1 -1
- {dagster_dingtalk-0.1.10b2.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.10b2.dist-info/RECORD +0 -8
- {dagster_dingtalk-0.1.10b2.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
@@ -11,23 +11,119 @@ 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
|
81
|
+
|
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
|
17
90
|
|
18
|
-
|
19
|
-
|
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
|

|
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=Z7TtBjliXrU9hglzohymYmg_Itd6PnogpvJE9xO7IjQ,9781
|
5
|
-
dagster_dingtalk/version.py,sha256=imdWu6hqscM5m8hVsqlHCfCyyqB6DbTnncbxy_25Udk,25
|
6
|
-
dagster_dingtalk-0.1.10b2.dist-info/METADATA,sha256=MrbdHJS2Ej8MDwmCuCVmXuMpD7dhME79klKzfps_XCA,1223
|
7
|
-
dagster_dingtalk-0.1.10b2.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
8
|
-
dagster_dingtalk-0.1.10b2.dist-info/RECORD,,
|
File without changes
|