dagster-dingtalk 0.1.23__py3-none-any.whl → 0.1.26__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.
@@ -1,422 +1,422 @@
1
- import base64
2
- import hashlib
3
- import hmac
4
- import re
5
- import time
6
- import urllib.parse
7
- from typing import Optional, Tuple, List, Literal
8
- import httpx
9
- from pydantic import Field
10
- from .app_client import DingTalkClient
11
- from dagster import ConfigurableResource, InitResourceContext
12
-
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
-
39
- class DingTalkWebhookResource(ConfigurableResource):
40
- """
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
- 别名,仅用作标记。默认值为 None。
52
- - **base_url** (str, optional):
53
- 通用地址,一般无需更改。默认值为 “https://oapi.dingtalk.com/robot/send”。
54
-
55
- ### 用例:
56
-
57
- ##### 1. 使用单个资源:
58
- ```python
59
- from dagster_dingtalk import DingTalkWebhookResource
60
- from dagster import op, In, OpExecutionContext, job, Definitions
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
- dingtalk_webhook.send_text(text)
66
-
67
- @job
68
- def job_send_text():
69
- op_send_text()
70
-
71
- defs = Definitions(
72
- jobs=[job_send_text],
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
- from dagster import op, In, OpExecutionContext, job, Definitions, schedule, RunRequest, RunConfig
82
-
83
- @op(required_resource_keys={"dingtalk_webhook"}, ins={"text": In(str)})
84
- def op_send_text(context:OpExecutionContext, text:str):
85
- dingtalk_webhook:DingTalkWebhookResource = context.resources.dingtalk_webhook
86
- dingtalk_webhook.send_text(text)
87
-
88
- @job
89
- def job_send_text():
90
- op_send_text()
91
-
92
- dingtalk_webhooks = {
93
- "Group1" : DingTalkWebhookResource(access_token="<access_token>", secret="<secret>", alias="Group1"),
94
- "Group2" : DingTalkWebhookResource(access_token="<access_token>", secret="<secret>", alias="Group2")
95
- }
96
-
97
- defs = Definitions(
98
- jobs=[job_send_text],
99
- resources={"dingtalk_webhook": DingTalkWebhookResource.configure_at_launch()}
100
- )
101
-
102
- @schedule(cron_schedule="20 9 * * *", job=job_send_text)
103
- def schedule_user_info():
104
- return RunRequest(run_config=RunConfig(
105
- ops={"op_send_text": {"inputs": {"text": "This a test text."}}},
106
- resources={"dingtalk": dingtalk_webhooks["Group1"]},
107
- ))
108
- ```
109
-
110
- ### 注意:
111
-
112
- 应该永远避免直接将密钥字符串直接配置给资源,这会导致在 dagster 前端用户界面暴露密钥。
113
- 应当从环境变量中读取密钥。你可以在代码中注册临时的环境变量,或从系统中引入环境变量。
114
-
115
- ```python
116
-
117
- import os
118
- from dagster import EnvVar
119
- from dagster_dingtalk import DingTalkWebhookResource
120
-
121
- # 直接在代码中注册临时的环境变量
122
- os.environ.update({'access_token_name': "<your-access_token>"})
123
- os.environ.update({'secret_name': "<your-secret>"})
124
-
125
- webhook = DingTalkWebhookResource(access_token=EnvVar("access_token_name"), secret=EnvVar("secret_name"))
126
- ```
127
- """
128
-
129
- access_token: str = Field(description="Webhook地址中的 access_token 部分")
130
- secret: Optional[str] = Field(default=None, description="如使用加签安全配置,需传签名密钥")
131
- alias: Optional[str] = Field(default=None, description="别名,标记用,无实际意义")
132
- base_url: str = Field(default="https://oapi.dingtalk.com/robot/send", description="Webhook的通用地址,无需更改")
133
-
134
- def webhook_url(self):
135
- """
136
- 实时生成加签或未加签的 Webhook URL。
137
-
138
- 钉钉API文档:
139
- https://open.dingtalk.com/document/robots/custom-robot-access
140
-
141
- :return:
142
- str: Webhook URL
143
- """
144
- if self.secret is None:
145
- return f"{self.base_url}?access_token={self.access_token}"
146
- else:
147
- timestamp = round(time.time() * 1000)
148
- hmac_code = hmac.new(
149
- self.secret.encode('utf-8'), f'{timestamp}\n{self.secret}'.encode('utf-8'), digestmod=hashlib.sha256
150
- ).digest()
151
- sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))
152
- return f"{self.base_url}?access_token={self.access_token}&timestamp={timestamp}&sign={sign}"
153
-
154
- @staticmethod
155
- def _gen_title(text):
156
- """
157
- 从文本截取前12个字符作为标题,并清理其中的 Markdown 格式字符。
158
-
159
- :param text: 原文
160
-
161
- :return:
162
- str: 标题
163
- """
164
- return re.sub(r'[\n#>* ]', '', text[:12])
165
-
166
- @staticmethod
167
- def __handle_response(response):
168
- """
169
- 处理钉钉 Webhook API 响应,根据 errcode 抛出相应的异常
170
-
171
- :param response: 钉钉Webhook API响应的JSON数据
172
- """
173
- errcode = str(response.json().get("errcode"))
174
- errmsg = response.json().get("errmsg")
175
-
176
- if errcode == "0":
177
- return True
178
- else:
179
- raise DingTalkWebhookException(errcode, errmsg)
180
-
181
- def send_text(self, text: str,
182
- at_mobiles:List[str]|None = None, at_user_ids:List[str]|None = None, at_all:bool = False):
183
- """
184
- 发送文本消息。
185
-
186
- 钉钉API文档:
187
- https://open.dingtalk.com/document/orgapp/custom-bot-send-message-type
188
-
189
- :param str text: 待发送文本
190
- :param List[str],optional at_mobiles: 需要 @ 的用户手机号。默认值为 None
191
- :param List[str],optional at_user_ids: 需要 @ 的用户 UserID。默认值为 None
192
- :param bool,optional at_all: 是否 @ 所有人。默认值为 False
193
-
194
- :raise DingTalkWebhookException:
195
-
196
- """
197
- at = {"isAtAll": at_all}
198
- if at_user_ids:
199
- at["atUserIds"] = at_user_ids
200
- if at_mobiles:
201
- at["atMobiles"] = at_mobiles
202
- response = httpx.post(url=self.webhook_url(), json={"msgtype": "text", "text": {"content": text}, "at": at})
203
- self.__handle_response(response)
204
-
205
- def send_link(self, text: str, message_url:str, title:str|None = None, pic_url:str = ""):
206
- """
207
- 发送 Link 消息。
208
-
209
- 钉钉API文档:
210
- https://open.dingtalk.com/document/orgapp/custom-bot-send-message-type
211
-
212
- :param str text: 待发送文本
213
- :param str message_url: 链接的 Url
214
- :param str,optional title: 标题,在通知和被引用时显示的简短信息。默认从文本中生成。
215
- :param str,optional pic_url: 图片的 Url,默认为 None
216
-
217
- :raise DingTalkWebhookException:
218
- """
219
- title = title or self._gen_title(text)
220
- response = httpx.post(
221
- url=self.webhook_url(),
222
- json={"msgtype": "link", "link": {"title": title, "text": text, "picUrl": pic_url, "messageUrl": message_url}}
223
- )
224
- self.__handle_response(response)
225
-
226
- def send_markdown(self, text: List[str]|str, title:str|None = None,
227
- at_mobiles:List[str]|None = None, at_user_ids:List[str]|None = None, at_all:bool = False):
228
- """
229
- 发送 Markdown 消息。支持的语法有:
230
- # 一级标题 ## 二级标题 ### 三级标题 #### 四级标题 ##### 五级标题 ###### 六级标题
231
- > 引用 **加粗** *斜体*
232
- [链接跳转](https://example.com/doc.html)
233
- ![图片预览](https://example.com/pic.jpg)
234
- - 无序列表
235
- 1. 有序列表
236
-
237
- 钉钉API文档:
238
- https://open.dingtalk.com/document/orgapp/custom-bot-send-message-type
239
-
240
- :param str text: 待发送文本
241
- :param str,optional title: 标题,在通知和被引用时显示的简短信息。默认从文本中生成。
242
- :param List[str],optional at_mobiles: 需要 @ 的用户手机号。默认值为 None
243
- :param List[str],optional at_user_ids: 需要 @ 的用户 UserID。默认值为 None
244
- :param bool,optional at_all: 是否 @ 所有人。默认值为 False
245
-
246
- :raise DingTalkWebhookException:
247
- """
248
- text = text if isinstance(text, str) else "\n\n".join(text)
249
- title = title or self._gen_title(text)
250
- at = {"isAtAll": at_all}
251
- if at_user_ids:
252
- at["atUserIds"] = at_user_ids
253
- if at_mobiles:
254
- at["atMobiles"] = at_mobiles
255
- response = httpx.post(url=self.webhook_url(),json={"msgtype": "markdown", "markdown": {"title": title, "text": text}, "at": at})
256
- self.__handle_response(response)
257
-
258
- def send_action_card(self, text: List[str]|str, title:str|None = None, btn_orientation:Literal["0","1"] = "0",
259
- single_jump:Tuple[str,str]|None = None, btns_jump:List[Tuple[str,str]]|None = None):
260
- """
261
- 发送跳转 ActionCard 消息。
262
-
263
- **注意:**
264
- 同时传 `single_jump` 和 `btns_jump`,仅 `single_jump` 生效。
265
-
266
- :param str text: 待发送文本,支持 Markdown 部分语法。
267
- :param str,optional title: 标题,在通知和被引用时显示的简短信息。默认从文本中生成。
268
- :param str,optional btn_orientation: 按钮排列方式,0-按钮竖直排列,1-按钮横向排列。默认值为 "0"
269
- :param Tuple[str,str],optional single_jump: 传此参数为单个按钮,元组内第一项为按钮的标题,第二项为按钮链接。
270
- :param Tuple[str,str],optional btns_jump: 传此参数为多个按钮,元组内第一项为按钮的标题,第二项为按钮链接。
271
-
272
- :raise DingTalkWebhookException:
273
- """
274
- text = text if isinstance(text, str) else "\n\n".join(text)
275
- title = title or self._gen_title(text)
276
- action_card = {"title": title, "text": text, "btnOrientation": str(btn_orientation)}
277
-
278
- if single_jump:
279
- action_card["singleTitle"], action_card["singleURL"] = single_jump
280
- if btns_jump:
281
- action_card["btns"] = [{"title": action_title, "actionURL": action_url} for action_title, action_url in btns_jump]
282
-
283
- response = httpx.post(url=self.webhook_url(), json={"msgtype": "actionCard", "actionCard": action_card})
284
- self.__handle_response(response)
285
-
286
- def send_feed_card(self, *args:Tuple[str,str,str]):
287
- """
288
- 发送 FeedCard 消息。
289
-
290
- :param Tuple[str,str,str],optional args: 可以传入任意个具有三个元素的元组,分别为 `(标题, 跳转链接, 缩略图链接)`
291
-
292
- :raise DingTalkWebhookException:
293
- """
294
- for a in args:
295
- print(a)
296
- links_data = [
297
- {"title": title, "messageURL": message_url, "picURL": pic_url}
298
- for title, message_url, pic_url in args
299
- ]
300
- response = httpx.post(url=self.webhook_url(), json={"msgtype": "feedCard", "feedCard": {"links": links_data}})
301
- self.__handle_response(response)
302
-
303
-
304
- class DingTalkAppResource(ConfigurableResource):
305
- """
306
- [钉钉服务端 API](https://open.dingtalk.com/document/orgapp/api-overview) 企业内部应用部分的第三方封装。
307
-
308
- 通过此资源,可以调用部分钉钉服务端 API。具体封装的 API 可以在 IDE 中通过引入 `DingTalkAppClient` 类来查看 IDE 提示:
309
-
310
- ```python
311
- from dagster_dingtalk import DingTalkAppClient
312
-
313
- dingtalk: DingTalkAppClient
314
- ```
315
-
316
- ### 配置项:
317
-
318
- - **AppID** (str):
319
- 应用应用唯一标识 AppID,作为缓存标识符使用。不传入则不缓存鉴权。
320
- - **AgentID** (int, optional):
321
- 原企业内部应用 AgentId ,部分 API 会使用到。默认值为 None
322
- - **AppName** (str, optional):
323
- 应用名。
324
- - **ClientId** (str):
325
- 应用的 Client ID ,原 AppKey 和 SuiteKey
326
- - **ClientSecret** (str):
327
- 应用的 Client Secret ,原 AppSecret 和 SuiteSecret
328
-
329
- ### 用例
330
-
331
- ##### 1. 使用单一的企业内部应用资源。
332
-
333
- ```python
334
- from dagster_dingtalk import DingTalkAppResource, DingTalkAppClient
335
- from dagster import op, In, OpExecutionContext, job, Definitions, EnvVar
336
-
337
- @op(required_resource_keys={"dingtalk"}, ins={"user_id": In(str)})
338
- def op_user_info(context:OpExecutionContext, user_id:str):
339
- dingtalk:DingTalkAppClient = context.resources.dingtalk
340
- result = dingtalk.通讯录管理.用户管理.查询用户详情(user_id).get('result')
341
- context.log.info(result)
342
-
343
- @job
344
- def job_user_info():
345
- op_user_info()
346
-
347
- defs = Definitions(
348
- jobs=[job_user_info],
349
- resources={"dingtalk": DingTalkAppResource(
350
- AppID = "<the-app-id>",
351
- ClientId = "<the-client-id>",
352
- ClientSecret = EnvVar("<the-client-secret-env-name>"),
353
- )})
354
- ```
355
-
356
- ##### 2. 启动时动态构建企业内部应用资源, 可参考 [Dagster文档 | 在启动时配置资源](https://docs.dagster.io/concepts/resources#configuring-resources-at-launch-time)
357
-
358
- ```python
359
- from dagster_dingtalk import DingTalkAppResource, DingTalkAppClient
360
- from dagster import op, In, OpExecutionContext, job, Definitions, schedule, RunRequest, RunConfig, EnvVar
361
-
362
- @op(required_resource_keys={"dingtalk"}, ins={"user_id": In(str)})
363
- def op_user_info(context:OpExecutionContext, user_id:str):
364
- dingtalk:DingTalkAppClient = context.resources.dingtalk
365
- result = dingtalk.通讯录管理.用户管理.查询用户详情(user_id).get('result')
366
- context.log.info(result)
367
-
368
- @job
369
- def job_user_info():
370
- op_user_info()
371
-
372
- dingtalk_apps = {
373
- "App1" : DingTalkAppResource(
374
- AppID = "<app-1-app-id>",
375
- ClientId = "<app-1-client-id>",
376
- ClientSecret = EnvVar("<app-1-client-secret-env-name>"),
377
- ),
378
- "App2" : DingTalkAppResource(
379
- AppID = "<app-2-app-id>",
380
- ClientId = "<app-2-client-id>",
381
- ClientSecret = EnvVar("<app-2-client-secret-env-name>"),
382
- )
383
- }
384
-
385
- defs = Definitions(jobs=[job_user_info], resources={"dingtalk": DingTalkAppResource.configure_at_launch()})
386
-
387
- @schedule(cron_schedule="20 9 * * *", job=job_user_info)
388
- def schedule_user_info():
389
- return RunRequest(run_config=RunConfig(
390
- ops={"op_user_info": {"inputs": {"user_id": "<the-user-id>"}}},
391
- resources={"dingtalk": dingtalk_apps["App1"]},
392
- ))
393
- ```
394
-
395
- ### 注意:
396
-
397
- 应该永远避免直接将密钥字符串直接配置给资源,这会导致在 dagster 前端用户界面暴露密钥。你可以在代码中注册临时的环境变量,或从系统中引入环境变量。
398
- """
399
-
400
- AppID: str = Field(description="应用应用唯一标识 AppID,作为缓存标识符使用。不传入则不缓存鉴权。")
401
- AgentID: Optional[int] = Field(default=None, description="原企业内部应用AgentId ,部分API会使用到。")
402
- AppName: Optional[str] = Field(default=None, description="应用名。")
403
- ClientId: str = Field(description="应用的 Client ID (原 AppKey 和 SuiteKey)")
404
- ClientSecret: str = Field(description="应用的 Client Secret (原 AppSecret 和 SuiteSecret)")
405
-
406
- @classmethod
407
- def _is_dagster_maintained(cls) -> bool:
408
- return False
409
-
410
- def create_resource(self, context: InitResourceContext) -> DingTalkClient:
411
- """
412
- 返回一个 `DingTalkClient` 实例。
413
- :param context:
414
- :return:
415
- """
416
- return DingTalkClient(
417
- app_id=self.AppID,
418
- agent_id=self.AgentID,
419
- app_name=self.AppName,
420
- client_id=self.ClientId,
421
- client_secret=self.ClientSecret
422
- )
1
+ import base64
2
+ import hashlib
3
+ import hmac
4
+ import re
5
+ import time
6
+ import urllib.parse
7
+ from typing import Optional, Tuple, List, Literal
8
+ import httpx
9
+ from pydantic import Field
10
+ from .app_client import DingTalkClient
11
+ from dagster import ConfigurableResource, InitResourceContext
12
+
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
+
39
+ class DingTalkWebhookResource(ConfigurableResource):
40
+ """
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
+ 别名,仅用作标记。默认值为 None。
52
+ - **base_url** (str, optional):
53
+ 通用地址,一般无需更改。默认值为 “https://oapi.dingtalk.com/robot/send”。
54
+
55
+ ### 用例:
56
+
57
+ ##### 1. 使用单个资源:
58
+ ```python
59
+ from dagster_dingtalk import DingTalkWebhookResource
60
+ from dagster import op, In, OpExecutionContext, job, Definitions
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
+ dingtalk_webhook.send_text(text)
66
+
67
+ @job
68
+ def job_send_text():
69
+ op_send_text()
70
+
71
+ defs = Definitions(
72
+ jobs=[job_send_text],
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
+ from dagster import op, In, OpExecutionContext, job, Definitions, schedule, RunRequest, RunConfig
82
+
83
+ @op(required_resource_keys={"dingtalk_webhook"}, ins={"text": In(str)})
84
+ def op_send_text(context:OpExecutionContext, text:str):
85
+ dingtalk_webhook:DingTalkWebhookResource = context.resources.dingtalk_webhook
86
+ dingtalk_webhook.send_text(text)
87
+
88
+ @job
89
+ def job_send_text():
90
+ op_send_text()
91
+
92
+ dingtalk_webhooks = {
93
+ "Group1" : DingTalkWebhookResource(access_token="<access_token>", secret="<secret>", alias="Group1"),
94
+ "Group2" : DingTalkWebhookResource(access_token="<access_token>", secret="<secret>", alias="Group2")
95
+ }
96
+
97
+ defs = Definitions(
98
+ jobs=[job_send_text],
99
+ resources={"dingtalk_webhook": DingTalkWebhookResource.configure_at_launch()}
100
+ )
101
+
102
+ @schedule(cron_schedule="20 9 * * *", job=job_send_text)
103
+ def schedule_user_info():
104
+ return RunRequest(run_config=RunConfig(
105
+ ops={"op_send_text": {"inputs": {"text": "This a test text."}}},
106
+ resources={"dingtalk": dingtalk_webhooks["Group1"]},
107
+ ))
108
+ ```
109
+
110
+ ### 注意:
111
+
112
+ 应该永远避免直接将密钥字符串直接配置给资源,这会导致在 dagster 前端用户界面暴露密钥。
113
+ 应当从环境变量中读取密钥。你可以在代码中注册临时的环境变量,或从系统中引入环境变量。
114
+
115
+ ```python
116
+
117
+ import os
118
+ from dagster import EnvVar
119
+ from dagster_dingtalk import DingTalkWebhookResource
120
+
121
+ # 直接在代码中注册临时的环境变量
122
+ os.environ.update({'access_token_name': "<your-access_token>"})
123
+ os.environ.update({'secret_name': "<your-secret>"})
124
+
125
+ webhook = DingTalkWebhookResource(access_token=EnvVar("access_token_name"), secret=EnvVar("secret_name"))
126
+ ```
127
+ """
128
+
129
+ access_token: str = Field(description="Webhook地址中的 access_token 部分")
130
+ secret: Optional[str] = Field(default=None, description="如使用加签安全配置,需传签名密钥")
131
+ alias: Optional[str] = Field(default=None, description="别名,标记用,无实际意义")
132
+ base_url: str = Field(default="https://oapi.dingtalk.com/robot/send", description="Webhook的通用地址,无需更改")
133
+
134
+ def webhook_url(self):
135
+ """
136
+ 实时生成加签或未加签的 Webhook URL。
137
+
138
+ 钉钉API文档:
139
+ https://open.dingtalk.com/document/robots/custom-robot-access
140
+
141
+ :return:
142
+ str: Webhook URL
143
+ """
144
+ if self.secret is None:
145
+ return f"{self.base_url}?access_token={self.access_token}"
146
+ else:
147
+ timestamp = round(time.time() * 1000)
148
+ hmac_code = hmac.new(
149
+ self.secret.encode('utf-8'), f'{timestamp}\n{self.secret}'.encode('utf-8'), digestmod=hashlib.sha256
150
+ ).digest()
151
+ sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))
152
+ return f"{self.base_url}?access_token={self.access_token}&timestamp={timestamp}&sign={sign}"
153
+
154
+ @staticmethod
155
+ def _gen_title(text):
156
+ """
157
+ 从文本截取前12个字符作为标题,并清理其中的 Markdown 格式字符。
158
+
159
+ :param text: 原文
160
+
161
+ :return:
162
+ str: 标题
163
+ """
164
+ return re.sub(r'[\n#>* ]', '', text[:12])
165
+
166
+ @staticmethod
167
+ def __handle_response(response):
168
+ """
169
+ 处理钉钉 Webhook API 响应,根据 errcode 抛出相应的异常
170
+
171
+ :param response: 钉钉Webhook API响应的JSON数据
172
+ """
173
+ errcode = str(response.json().get("errcode"))
174
+ errmsg = response.json().get("errmsg")
175
+
176
+ if errcode == "0":
177
+ return True
178
+ else:
179
+ raise DingTalkWebhookException(errcode, errmsg)
180
+
181
+ def send_text(self, text: str,
182
+ at_mobiles:List[str]|None = None, at_user_ids:List[str]|None = None, at_all:bool = False):
183
+ """
184
+ 发送文本消息。
185
+
186
+ 钉钉API文档:
187
+ https://open.dingtalk.com/document/orgapp/custom-bot-send-message-type
188
+
189
+ :param str text: 待发送文本
190
+ :param List[str],optional at_mobiles: 需要 @ 的用户手机号。默认值为 None
191
+ :param List[str],optional at_user_ids: 需要 @ 的用户 UserID。默认值为 None
192
+ :param bool,optional at_all: 是否 @ 所有人。默认值为 False
193
+
194
+ :raise DingTalkWebhookException:
195
+
196
+ """
197
+ at = {"isAtAll": at_all}
198
+ if at_user_ids:
199
+ at["atUserIds"] = at_user_ids
200
+ if at_mobiles:
201
+ at["atMobiles"] = at_mobiles
202
+ response = httpx.post(url=self.webhook_url(), json={"msgtype": "text", "text": {"content": text}, "at": at})
203
+ self.__handle_response(response)
204
+
205
+ def send_link(self, text: str, message_url:str, title:str|None = None, pic_url:str = ""):
206
+ """
207
+ 发送 Link 消息。
208
+
209
+ 钉钉API文档:
210
+ https://open.dingtalk.com/document/orgapp/custom-bot-send-message-type
211
+
212
+ :param str text: 待发送文本
213
+ :param str message_url: 链接的 Url
214
+ :param str,optional title: 标题,在通知和被引用时显示的简短信息。默认从文本中生成。
215
+ :param str,optional pic_url: 图片的 Url,默认为 None
216
+
217
+ :raise DingTalkWebhookException:
218
+ """
219
+ title = title or self._gen_title(text)
220
+ response = httpx.post(
221
+ url=self.webhook_url(),
222
+ json={"msgtype": "link", "link": {"title": title, "text": text, "picUrl": pic_url, "messageUrl": message_url}}
223
+ )
224
+ self.__handle_response(response)
225
+
226
+ def send_markdown(self, text: List[str]|str, title:str|None = None,
227
+ at_mobiles:List[str]|None = None, at_user_ids:List[str]|None = None, at_all:bool = False):
228
+ """
229
+ 发送 Markdown 消息。支持的语法有:
230
+ # 一级标题 ## 二级标题 ### 三级标题 #### 四级标题 ##### 五级标题 ###### 六级标题
231
+ > 引用 **加粗** *斜体*
232
+ [链接跳转](https://example.com/doc.html)
233
+ ![图片预览](https://example.com/pic.jpg)
234
+ - 无序列表
235
+ 1. 有序列表
236
+
237
+ 钉钉API文档:
238
+ https://open.dingtalk.com/document/orgapp/custom-bot-send-message-type
239
+
240
+ :param str text: 待发送文本
241
+ :param str,optional title: 标题,在通知和被引用时显示的简短信息。默认从文本中生成。
242
+ :param List[str],optional at_mobiles: 需要 @ 的用户手机号。默认值为 None
243
+ :param List[str],optional at_user_ids: 需要 @ 的用户 UserID。默认值为 None
244
+ :param bool,optional at_all: 是否 @ 所有人。默认值为 False
245
+
246
+ :raise DingTalkWebhookException:
247
+ """
248
+ text = text if isinstance(text, str) else "\n\n".join(text)
249
+ title = title or self._gen_title(text)
250
+ at = {"isAtAll": at_all}
251
+ if at_user_ids:
252
+ at["atUserIds"] = at_user_ids
253
+ if at_mobiles:
254
+ at["atMobiles"] = at_mobiles
255
+ response = httpx.post(url=self.webhook_url(),json={"msgtype": "markdown", "markdown": {"title": title, "text": text}, "at": at})
256
+ self.__handle_response(response)
257
+
258
+ def send_action_card(self, text: List[str]|str, title:str|None = None, btn_orientation:Literal["0","1"] = "0",
259
+ single_jump:Tuple[str,str]|None = None, btns_jump:List[Tuple[str,str]]|None = None):
260
+ """
261
+ 发送跳转 ActionCard 消息。
262
+
263
+ **注意:**
264
+ 同时传 `single_jump` 和 `btns_jump`,仅 `single_jump` 生效。
265
+
266
+ :param str text: 待发送文本,支持 Markdown 部分语法。
267
+ :param str,optional title: 标题,在通知和被引用时显示的简短信息。默认从文本中生成。
268
+ :param str,optional btn_orientation: 按钮排列方式,0-按钮竖直排列,1-按钮横向排列。默认值为 "0"
269
+ :param Tuple[str,str],optional single_jump: 传此参数为单个按钮,元组内第一项为按钮的标题,第二项为按钮链接。
270
+ :param Tuple[str,str],optional btns_jump: 传此参数为多个按钮,元组内第一项为按钮的标题,第二项为按钮链接。
271
+
272
+ :raise DingTalkWebhookException:
273
+ """
274
+ text = text if isinstance(text, str) else "\n\n".join(text)
275
+ title = title or self._gen_title(text)
276
+ action_card = {"title": title, "text": text, "btnOrientation": str(btn_orientation)}
277
+
278
+ if single_jump:
279
+ action_card["singleTitle"], action_card["singleURL"] = single_jump
280
+ if btns_jump:
281
+ action_card["btns"] = [{"title": action_title, "actionURL": action_url} for action_title, action_url in btns_jump]
282
+
283
+ response = httpx.post(url=self.webhook_url(), json={"msgtype": "actionCard", "actionCard": action_card})
284
+ self.__handle_response(response)
285
+
286
+ def send_feed_card(self, *args:Tuple[str,str,str]):
287
+ """
288
+ 发送 FeedCard 消息。
289
+
290
+ :param Tuple[str,str,str],optional args: 可以传入任意个具有三个元素的元组,分别为 `(标题, 跳转链接, 缩略图链接)`
291
+
292
+ :raise DingTalkWebhookException:
293
+ """
294
+ for a in args:
295
+ print(a)
296
+ links_data = [
297
+ {"title": title, "messageURL": message_url, "picURL": pic_url}
298
+ for title, message_url, pic_url in args
299
+ ]
300
+ response = httpx.post(url=self.webhook_url(), json={"msgtype": "feedCard", "feedCard": {"links": links_data}})
301
+ self.__handle_response(response)
302
+
303
+
304
+ class DingTalkAppResource(ConfigurableResource):
305
+ """
306
+ [钉钉服务端 API](https://open.dingtalk.com/document/orgapp/api-overview) 企业内部应用部分的第三方封装。
307
+
308
+ 通过此资源,可以调用部分钉钉服务端 API。具体封装的 API 可以在 IDE 中通过引入 `DingTalkAppClient` 类来查看 IDE 提示:
309
+
310
+ ```python
311
+ from dagster_dingtalk import DingTalkAppClient
312
+
313
+ dingtalk: DingTalkAppClient
314
+ ```
315
+
316
+ ### 配置项:
317
+
318
+ - **AppID** (str):
319
+ 应用应用唯一标识 AppID,作为缓存标识符使用。不传入则不缓存鉴权。
320
+ - **AgentID** (int, optional):
321
+ 原企业内部应用 AgentId ,部分 API 会使用到。默认值为 None
322
+ - **AppName** (str, optional):
323
+ 应用名。
324
+ - **ClientId** (str):
325
+ 应用的 Client ID ,原 AppKey 和 SuiteKey
326
+ - **ClientSecret** (str):
327
+ 应用的 Client Secret ,原 AppSecret 和 SuiteSecret
328
+
329
+ ### 用例
330
+
331
+ ##### 1. 使用单一的企业内部应用资源。
332
+
333
+ ```python
334
+ from dagster_dingtalk import DingTalkAppResource, DingTalkAppClient
335
+ from dagster import op, In, OpExecutionContext, job, Definitions, EnvVar
336
+
337
+ @op(required_resource_keys={"dingtalk"}, ins={"user_id": In(str)})
338
+ def op_user_info(context:OpExecutionContext, user_id:str):
339
+ dingtalk:DingTalkAppClient = context.resources.dingtalk
340
+ result = dingtalk.通讯录管理.用户管理.查询用户详情(user_id).get('result')
341
+ context.log.info(result)
342
+
343
+ @job
344
+ def job_user_info():
345
+ op_user_info()
346
+
347
+ defs = Definitions(
348
+ jobs=[job_user_info],
349
+ resources={"dingtalk": DingTalkAppResource(
350
+ AppID = "<the-app-id>",
351
+ ClientId = "<the-client-id>",
352
+ ClientSecret = EnvVar("<the-client-secret-env-name>"),
353
+ )})
354
+ ```
355
+
356
+ ##### 2. 启动时动态构建企业内部应用资源, 可参考 [Dagster文档 | 在启动时配置资源](https://docs.dagster.io/concepts/resources#configuring-resources-at-launch-time)
357
+
358
+ ```python
359
+ from dagster_dingtalk import DingTalkAppResource, DingTalkAppClient
360
+ from dagster import op, In, OpExecutionContext, job, Definitions, schedule, RunRequest, RunConfig, EnvVar
361
+
362
+ @op(required_resource_keys={"dingtalk"}, ins={"user_id": In(str)})
363
+ def op_user_info(context:OpExecutionContext, user_id:str):
364
+ dingtalk:DingTalkAppClient = context.resources.dingtalk
365
+ result = dingtalk.通讯录管理.用户管理.查询用户详情(user_id).get('result')
366
+ context.log.info(result)
367
+
368
+ @job
369
+ def job_user_info():
370
+ op_user_info()
371
+
372
+ dingtalk_apps = {
373
+ "App1" : DingTalkAppResource(
374
+ AppID = "<app-1-app-id>",
375
+ ClientId = "<app-1-client-id>",
376
+ ClientSecret = EnvVar("<app-1-client-secret-env-name>"),
377
+ ),
378
+ "App2" : DingTalkAppResource(
379
+ AppID = "<app-2-app-id>",
380
+ ClientId = "<app-2-client-id>",
381
+ ClientSecret = EnvVar("<app-2-client-secret-env-name>"),
382
+ )
383
+ }
384
+
385
+ defs = Definitions(jobs=[job_user_info], resources={"dingtalk": DingTalkAppResource.configure_at_launch()})
386
+
387
+ @schedule(cron_schedule="20 9 * * *", job=job_user_info)
388
+ def schedule_user_info():
389
+ return RunRequest(run_config=RunConfig(
390
+ ops={"op_user_info": {"inputs": {"user_id": "<the-user-id>"}}},
391
+ resources={"dingtalk": dingtalk_apps["App1"]},
392
+ ))
393
+ ```
394
+
395
+ ### 注意:
396
+
397
+ 应该永远避免直接将密钥字符串直接配置给资源,这会导致在 dagster 前端用户界面暴露密钥。你可以在代码中注册临时的环境变量,或从系统中引入环境变量。
398
+ """
399
+
400
+ AppID: str = Field(description="应用应用唯一标识 AppID,作为缓存标识符使用。不传入则不缓存鉴权。")
401
+ AgentID: Optional[int] = Field(default=None, description="原企业内部应用AgentId ,部分API会使用到。")
402
+ AppName: Optional[str] = Field(default=None, description="应用名。")
403
+ ClientId: str = Field(description="应用的 Client ID (原 AppKey 和 SuiteKey)")
404
+ ClientSecret: str = Field(description="应用的 Client Secret (原 AppSecret 和 SuiteSecret)")
405
+
406
+ @classmethod
407
+ def _is_dagster_maintained(cls) -> bool:
408
+ return False
409
+
410
+ def create_resource(self, context: InitResourceContext) -> DingTalkClient:
411
+ """
412
+ 返回一个 `DingTalkClient` 实例。
413
+ :param context:
414
+ :return:
415
+ """
416
+ return DingTalkClient(
417
+ app_id=self.AppID,
418
+ agent_id=self.AgentID,
419
+ app_name=self.AppName,
420
+ client_id=self.ClientId,
421
+ client_secret=self.ClientSecret
422
+ )