dagster-dingtalk 0.1.10b2__tar.gz → 0.1.12__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dagster-dingtalk
3
- Version: 0.1.10b2
3
+ Version: 0.1.12
4
4
  Summary: A dagster plugin for the DingTalk
5
5
  Author: YiZixuan
6
6
  Author-email: sqkkyzx@qq.com
@@ -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
 
@@ -0,0 +1,327 @@
1
+ import logging
2
+ import pickle
3
+ import time
4
+ from datetime import datetime
5
+ from enum import Enum
6
+ from pathlib import Path
7
+ from typing import List, Literal, Dict, Tuple
8
+ from httpx import Client
9
+
10
+
11
+ # noinspection NonAsciiCharacters
12
+ class DingTalkClient:
13
+ def __init__(self, app_id: str, client_id: str, client_secret: str, app_name: str|None = None, agent_id: int|None = None):
14
+ self.app_id: str = app_id
15
+ self.app_name: str|None = app_name
16
+ self.agent_id: int|None = agent_id
17
+ self.client_id: str = client_id
18
+ self.__client_secret: str = client_secret
19
+ self.robot_code: str = client_id
20
+
21
+ access_token: str = self.__get_access_token()
22
+ self.api: Client = Client(base_url="https://api.dingtalk.com/", headers={"x-acs-dingtalk-access-token": access_token})
23
+ self.oapi: Client = Client(base_url="https://oapi.dingtalk.com/", params={"access_token": access_token})
24
+
25
+ self.智能人事 = 智能人事_API(self)
26
+ self.通讯录管理 = 通讯录管理_API(self)
27
+ self.文档文件 = 文档文件_API(self)
28
+ self.互动卡片 = 互动卡片_API(self)
29
+
30
+ def __get_access_token(self) -> str:
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
36
+
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
+ all_access_token = pickle.loads(f.read())
44
+ except Exception as e:
45
+ logging.error(e)
46
+ renew_reason = "鉴权缓存读取或解析错误"
47
+
48
+ if all_access_token:
49
+ app_access_token = all_access_token.get(self.app_id)
50
+ access_token = app_access_token.get('access_token')
51
+ expire_in = app_access_token.get('expire_in')
52
+ else:
53
+ renew_reason = f"鉴权缓存不存在该应用 {self.app_name}<{self.app_id}>"
54
+
55
+ if not access_token:
56
+ renew_reason = F"应用 {self.app_name}<{self.app_id}> 的鉴权缓存无效"
57
+ if expire_in < int(time.time()):
58
+ renew_reason = F"应用 {self.app_name}<{self.app_id}> 的鉴权缓存过期"
59
+
60
+ if renew_reason is None:
61
+ return access_token
62
+ else:
63
+ logging.warning(renew_reason)
64
+ response = Client().post(
65
+ url="https://api.dingtalk.com/v1.0/oauth2/accessToken",
66
+ json={"appKey": self.client_id, "appSecret": self.__client_secret},
67
+ )
68
+ access_token:str = response.json().get("accessToken")
69
+ expire_in:int = response.json().get("expireIn") + int(time.time()) - 60
70
+ with open(access_token_cache, 'wb') as f:
71
+ all_access_token[self.app_id] = {
72
+ "access_token": access_token,
73
+ "expire_in": expire_in,
74
+ }
75
+ f.write(pickle.dumps(all_access_token))
76
+ return access_token
77
+
78
+ # noinspection NonAsciiCharacters
79
+ class 智能人事_API:
80
+ def __init__(self, _client:DingTalkClient):
81
+ self.__client:DingTalkClient = _client
82
+ self.花名册 = 智能人事_花名册_API(_client)
83
+ self.员工管理 = 智能人事_员工管理_API(_client)
84
+
85
+ # noinspection NonAsciiCharacters
86
+ class 智能人事_花名册_API:
87
+ def __init__(self, _client:DingTalkClient):
88
+ self.__client:DingTalkClient = _client
89
+
90
+ def 获取花名册元数据(self) -> dict:
91
+ response = self.__client.oapi.post(
92
+ url="/topapi/smartwork/hrm/roster/meta/get",
93
+ json={"agentid": self.__client.agent_id},
94
+ )
95
+ return response.json()
96
+
97
+ def 获取员工花名册字段信息(self, user_id_list:List[str], field_filter_list:List[str]|None = None, text_to_select_convert:bool|None = None) -> dict:
98
+ body_dict = {"userIdList": user_id_list, "appAgentId": self.__client.agent_id}
99
+ if field_filter_list is not None:
100
+ body_dict["fieldFilterList"] = field_filter_list
101
+ if text_to_select_convert is not None:
102
+ body_dict["text2SelectConvert"] = text_to_select_convert
103
+
104
+ response = self.__client.api.post(url="/topapi/smartwork/hrm/roster/meta/get", json=body_dict, )
105
+ return response.json()
106
+
107
+ # noinspection NonAsciiCharacters
108
+ class 智能人事_员工管理_API:
109
+ def __init__(self, _client:DingTalkClient):
110
+ self.__client:DingTalkClient = _client
111
+
112
+ # noinspection NonAsciiCharacters
113
+ class 在职员工状态(Enum):
114
+ 试用期: '2'
115
+ 正式: '3'
116
+ 待离职: '5'
117
+ 无状态: '-1'
118
+
119
+ def 获取待入职员工列表(self, offset:int, size:int) -> dict:
120
+ response = self.__client.oapi.post(
121
+ "/topapi/smartwork/hrm/employee/querypreentry",
122
+ json={"offset": offset, "size": size},
123
+ )
124
+ return response.json()
125
+
126
+ def 获取在职员工列表(self, status_list:List[在职员工状态], offset:int, size:int) -> dict:
127
+ response = self.__client.oapi.post(
128
+ "/topapi/smartwork/hrm/employee/querypreentry",
129
+ json={"status_list": status_list, "offset": offset, "size": size},
130
+ )
131
+ return response.json()
132
+
133
+ def 获取离职员工列表(self, next_token:int, max_results:int) -> dict:
134
+ response = self.__client.api.get(
135
+ "/v1.0/hrm/employees/dismissions",
136
+ params={"nextToken": next_token, "maxResults": max_results},
137
+ )
138
+ return response.json()
139
+
140
+ def 批量获取员工离职信息(self, user_id_list:List[str]) -> dict:
141
+ response = self.__client.api.get(
142
+ "/v1.0/hrm/employees/dimissionInfo",
143
+ params={"userIdList": user_id_list},
144
+ )
145
+ return response.json()
146
+
147
+ # noinspection NonAsciiCharacters
148
+ class 通讯录管理_API:
149
+ def __init__(self, _client:DingTalkClient):
150
+ self.__client:DingTalkClient = _client
151
+ self.用户管理 = 通讯录管理_用户管理_API(_client)
152
+ self.部门管理 = 通讯录管理_部门管理_API(_client)
153
+
154
+ def 查询用户详情(self, user_id:str, language:str = "zh_CN"):
155
+ return self.用户管理.查询用户详情(user_id, language)
156
+
157
+ # noinspection NonAsciiCharacters
158
+ class 通讯录管理_用户管理_API:
159
+ def __init__(self, _client:DingTalkClient):
160
+ self.__client:DingTalkClient = _client
161
+
162
+ def 查询用户详情(self, user_id:str, language:str = "zh_CN") -> dict:
163
+ response = self.__client.oapi.post(url="/topapi/v2/user/get", json={"language": language, "userid": user_id})
164
+ return response.json()
165
+
166
+ def 查询离职记录列表(self, start_time:datetime, end_time:datetime|None, next_token:str, max_results:int) -> dict:
167
+ params = {"startTime": start_time.strftime("%Y-%m-%dT%H:%M:%SZ"), "nextToken": next_token, "maxResults": max_results}
168
+ if end_time is not None:
169
+ params["endTime"] = end_time.strftime("%Y-%m-%dT%H:%M:%SZ")
170
+ response = self.__client.api.get(url="/v1.0/contact/empLeaveRecords", params=params)
171
+ return response.json()
172
+
173
+ # noinspection NonAsciiCharacters
174
+ class 通讯录管理_部门管理_API:
175
+ def __init__(self, _client:DingTalkClient):
176
+ self.__client:DingTalkClient = _client
177
+
178
+ def 获取部门详情(self, dept_id: int, language:str = "zh_CN") -> dict:
179
+ """
180
+ 调用本接口,根据部门ID获取指定部门详情。
181
+
182
+ https://open.dingtalk.com/document/orgapp/query-department-details0-v2
183
+
184
+ :param dept_id: 部门 ID ,根部门 ID 为 1。
185
+ :param language: 通讯录语言。zh_CN en_US
186
+ """
187
+ response = self.__client.oapi.post(
188
+ url="/topapi/v2/department/get",
189
+ json={"language": language, "dept_id": dept_id}
190
+ )
191
+ return response.json()
192
+
193
+ def 获取部门列表(self, dept_id: int, language:str = "zh_CN"):
194
+ """
195
+ 调用本接口,获取下一级部门基础信息。
196
+
197
+ https://open.dingtalk.com/document/orgapp/obtain-the-department-list-v2
198
+
199
+ :param dept_id: 部门 ID ,根部门 ID 为 1。
200
+ :param language: 通讯录语言。zh_CN en_US
201
+ """
202
+ response = self.__client.oapi.post(
203
+ url="/topapi/v2/department/listsub",
204
+ json={"language": language, "dept_id": dept_id}
205
+ )
206
+ return response.json()
207
+
208
+ # noinspection NonAsciiCharacters
209
+ class 文档文件_API:
210
+ def __init__(self, _client:DingTalkClient):
211
+ self.__client:DingTalkClient = _client
212
+ self.媒体文件 = 文档文件_媒体文件_API(_client)
213
+
214
+ # noinspection NonAsciiCharacters
215
+ class 文档文件_媒体文件_API:
216
+ def __init__(self, _client:DingTalkClient):
217
+ self.__client:DingTalkClient = _client
218
+
219
+ def 上传媒体文件(self, file_path:Path|str, media_type:Literal['image', 'voice', 'video', 'file']) -> dict:
220
+ """
221
+ 调用本接口,上传图片、语音媒体资源文件以及普通文件,接口返回媒体资源标识 media_id。
222
+
223
+ https://open.dingtalk.com/document/orgapp/upload-media-files
224
+
225
+ :param file_path: 本地文件路径
226
+ :param media_type: 媒体类型,支持 'image', 'voice', 'video', 'file'
227
+
228
+ :return:
229
+ {
230
+ "errcode": 0,
231
+ "errmsg": "ok",
232
+ "media_id": "$iAEKAqDBgTNAk",
233
+ "created_at": 1605863153573,
234
+ "type": "image"
235
+ }
236
+ """
237
+ with open(file_path, 'rb') as f:
238
+ response = self.__client.oapi.post(url=f"/media/upload?type={media_type}", files={'media': f})
239
+ return response.json()
240
+
241
+ # noinspection NonAsciiCharacters
242
+ class 互动卡片_API:
243
+ def __init__(self, _client:DingTalkClient):
244
+ self.__client:DingTalkClient = _client
245
+
246
+ def 创建并投放卡片(
247
+ self, search_type_name: str, search_desc: str, card_template_id: str, card_param_map: dict,
248
+ alert_content: str, open_space_ids: List[str], out_track_id: str, support_forward: bool = True,
249
+ call_back_type: str = "STREAM", expired_time_millis:int = 0
250
+ ) -> dict:
251
+ """
252
+ 创建并投放卡片。当前仅支持 IM群聊, IM机器人单聊, 吊顶 三种场域类型。
253
+
254
+ https://open.dingtalk.com/document/orgapp/create-and-deliver-cards
255
+
256
+ :param card_template_id: 卡片模板 ID
257
+ :param open_space_ids: 卡片投放场域 Id
258
+ :param out_track_id: 卡片唯一标识
259
+ :param card_param_map: 卡片数据
260
+ :param search_type_name: 卡片类型名
261
+ :param search_desc: 卡片消息展示
262
+ :param alert_content: 通知内容
263
+ :param call_back_type: 回调模式
264
+ :param support_forward: 是否支持转发
265
+ :param expired_time_millis: 吊顶投放过期时间。当投放内容为吊顶时必须传参。
266
+ """
267
+
268
+ open_space_id = f"dtv1.card//{';'.join(open_space_ids)}"
269
+
270
+ payload = {
271
+ "cardTemplateId": card_template_id,
272
+ "outTrackId": out_track_id,
273
+ "openSpaceId": open_space_id,
274
+ "callbackType": call_back_type,
275
+ "cardData": {"cardParamMap": card_param_map}
276
+ }
277
+
278
+ open_space_model = {
279
+ "supportForward": support_forward,
280
+ "searchSupport": {"searchTypeName": search_type_name, "searchDesc": search_desc},
281
+ "notification": {"alertContent": alert_content, "notificationOff": False}
282
+ }
283
+
284
+ if 'IM_GROUP' in open_space_id.upper():
285
+ payload["imGroupOpenSpaceModel"] = open_space_model
286
+ payload["imGroupOpenDeliverModel"] = {"robotCode": self.__client.robot_code}
287
+
288
+ if 'IM_ROBOT' in open_space_id.upper():
289
+ payload["imRobotOpenSpaceModel"] = open_space_model
290
+ payload["imRobotOpenDeliverModel"] = {"spaceType": "IM_ROBOT", "robotCode": self.__client.robot_code}
291
+
292
+ if 'ONE_BOX' in open_space_id.upper():
293
+ if expired_time_millis == 0:
294
+ expired_time_millis = int(time.time()+3600)*1000
295
+ payload["topOpenSpaceModel"] = {"spaceType": "ONE_BOX"}
296
+ payload["topOpenDeliverModel"] = {"platforms": ["android","ios","win","mac"], "expiredTimeMillis": expired_time_millis,}
297
+
298
+ response = self.__client.api.post(
299
+ url="/v1.0/card/instances/createAndDeliver",
300
+ json=payload
301
+ )
302
+
303
+ return response.json()
304
+
305
+ def 更新卡片(self, out_track_id: str, card_param_map: dict, update_card_data_by_key:bool=True) -> dict:
306
+ """
307
+ 调用本接口,实现主动更新卡片数据。
308
+
309
+ https://open.dingtalk.com/document/orgapp/interactive-card-update-interface
310
+
311
+ :param out_track_id: 外部卡片实例Id。
312
+ :param card_param_map: 卡片模板内容。
313
+ :param update_card_data_by_key: True-按 key 更新 cardData 数据 False-覆盖更新 cardData 数据
314
+ :return:
315
+ {success: bool, result: bool}
316
+ """
317
+
318
+ response = self.__client.api.put(
319
+ url="/v1.0/card/instances",
320
+ json={
321
+ "outTrackId": out_track_id,
322
+ "cardData": {"cardParamMap": card_param_map},
323
+ "cardUpdateOptions": {"updateCardDataByKey": update_card_data_by_key}
324
+ }
325
+ )
326
+
327
+ return response.json()
@@ -0,0 +1,413 @@
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
+ 如提供别名,可以在使用 `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
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"))
120
+
121
+
122
+ """
123
+
124
+ access_token: str = Field(description="Webhook地址中的 access_token 部分")
125
+ secret: Optional[str] = Field(default=None, description="如使用加签安全配置,需传签名密钥")
126
+ alias: Optional[str] = Field(default=None, description="别名,标记用,无实际意义")
127
+ base_url: str = Field(default="https://oapi.dingtalk.com/robot/send", description="Webhook的通用地址,无需更改")
128
+
129
+ def webhook_url(self):
130
+ """
131
+ 实时生成加签或未加签的 Webhook URL。
132
+
133
+ 钉钉API文档:
134
+ https://open.dingtalk.com/document/robots/custom-robot-access
135
+
136
+ :return:
137
+ str: Webhook URL
138
+ """
139
+ if self.secret is None:
140
+ return f"{self.base_url}?access_token={self.access_token}"
141
+ else:
142
+ timestamp = round(time.time() * 1000)
143
+ hmac_code = hmac.new(
144
+ self.secret.encode('utf-8'), f'{timestamp}\n{self.secret}'.encode('utf-8'), digestmod=hashlib.sha256
145
+ ).digest()
146
+ sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))
147
+ return f"{self.base_url}?access_token={self.access_token}&timestamp={timestamp}&sign={sign}"
148
+
149
+ @staticmethod
150
+ def _gen_title(text):
151
+ """
152
+ 从文本截取前12个字符作为标题,并清理其中的 Markdown 格式字符。
153
+
154
+ :param text: 原文
155
+
156
+ :return:
157
+ str: 标题
158
+ """
159
+ return re.sub(r'[\n#>* ]', '', text[:12])
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
+
176
+ def send_text(self, text: str,
177
+ at_mobiles:List[str]|None = None, at_user_ids:List[str]|None = None, at_all:bool = False):
178
+ """
179
+ 发送文本消息。
180
+
181
+ 钉钉API文档:
182
+ https://open.dingtalk.com/document/orgapp/custom-bot-send-message-type
183
+
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
+
191
+ """
192
+ at = {"isAtAll": at_all}
193
+ if at_user_ids:
194
+ at["atUserIds"] = at_user_ids
195
+ if at_mobiles:
196
+ at["atMobiles"] = at_mobiles
197
+ response = httpx.post(url=self.webhook_url(), json={"msgtype": "text", "text": {"content": text}, "at": at})
198
+ self.__handle_response(response)
199
+
200
+ def send_link(self, text: str, message_url:str, title:str|None = None, pic_url:str = ""):
201
+ """
202
+ 发送 Link 消息。
203
+
204
+ 钉钉API文档:
205
+ https://open.dingtalk.com/document/orgapp/custom-bot-send-message-type
206
+
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:
213
+ """
214
+ title = title or self._gen_title(text)
215
+ response = httpx.post(
216
+ url=self.webhook_url(),
217
+ json={"msgtype": "link", "link": {"title": title, "text": text, "picUrl": pic_url, "messageUrl": message_url}}
218
+ )
219
+ self.__handle_response(response)
220
+
221
+ def send_markdown(self, text: List[str]|str, title:str|None = None,
222
+ at_mobiles:List[str]|None = None, at_user_ids:List[str]|None = None, at_all:bool = False):
223
+ """
224
+ 发送 Markdown 消息。支持的语法有:
225
+ # 一级标题 ## 二级标题 ### 三级标题 #### 四级标题 ##### 五级标题 ###### 六级标题
226
+ > 引用 **加粗** *斜体*
227
+ [链接跳转](https://example.com/doc.html)
228
+ ![图片预览](https://example.com/pic.jpg)
229
+ - 无序列表
230
+ 1. 有序列表
231
+
232
+ 钉钉API文档:
233
+ https://open.dingtalk.com/document/orgapp/custom-bot-send-message-type
234
+
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:
242
+ """
243
+ text = text if isinstance(text, str) else "\n\n".join(text)
244
+ title = title or self._gen_title(text)
245
+ at = {"isAtAll": at_all}
246
+ if at_user_ids:
247
+ at["atUserIds"] = at_user_ids
248
+ if at_mobiles:
249
+ at["atMobiles"] = at_mobiles
250
+ response = httpx.post(url=self.webhook_url(),json={"msgtype": "markdown", "markdown": {"title": title, "text": text}, "at": at})
251
+ self.__handle_response(response)
252
+
253
+ def send_action_card(self, text: List[str]|str, title:str|None = None, btn_orientation:Literal["0","1"] = "0",
254
+ single_jump:Tuple[str,str]|None = None, btns_jump:List[Tuple[str,str]]|None = None):
255
+ """
256
+ 发送跳转 ActionCard 消息。
257
+
258
+ **注意:**
259
+ 同时传 `single_jump` 和 `btns_jump`,仅 `single_jump` 生效。
260
+
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: 传此参数为多个按钮,元组内第一项为按钮的标题,第二项为按钮链接。
266
+
267
+ :raise DingTalkWebhookException:
268
+ """
269
+ text = text if isinstance(text, str) else "\n\n".join(text)
270
+ title = title or self._gen_title(text)
271
+ action_card = {"title": title, "text": text, "btnOrientation": str(btn_orientation)}
272
+
273
+ if single_jump:
274
+ action_card["singleTitle"], action_card["singleURL"] = single_jump
275
+ if btns_jump:
276
+ action_card["btns"] = [{"title": action_title, "actionURL": action_url} for action_title, action_url in btns_jump]
277
+
278
+ response = httpx.post(url=self.webhook_url(), json={"msgtype": "actionCard", "actionCard": action_card})
279
+ self.__handle_response(response)
280
+
281
+ def send_feed_card(self, *args:Tuple[str,str,str]):
282
+ """
283
+ 发送 FeedCard 消息。
284
+
285
+ :param Tuple[str,str,str],optional args: 可以传入任意个具有三个元素的元组,分别为 `(标题, 跳转链接, 缩略图链接)`
286
+
287
+ :raise DingTalkWebhookException:
288
+ """
289
+ for a in args:
290
+ print(a)
291
+ links_data = [
292
+ {"title": title, "messageURL": message_url, "picURL": pic_url}
293
+ for title, message_url, pic_url in args
294
+ ]
295
+ response = httpx.post(url=self.webhook_url(), json={"msgtype": "feedCard", "feedCard": {"links": links_data}})
296
+ self.__handle_response(response)
297
+
298
+
299
+ class DingTalkAppResource(ConfigurableResource):
300
+ """
301
+ [钉钉服务端 API](https://open.dingtalk.com/document/orgapp/api-overview) 企业内部应用部分的第三方封装。
302
+
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
+ 应当从环境变量中读取密钥。你可以在代码中注册临时的环境变量,或从系统中引入环境变量。
388
+
389
+ """
390
+
391
+ AppID: str = Field(description="应用应用唯一标识 AppID,作为缓存标识符使用。不传入则不缓存鉴权。")
392
+ AgentID: Optional[int] = Field(default=None, description="原企业内部应用AgentId ,部分API会使用到。")
393
+ AppName: Optional[str] = Field(default=None, description="应用名。")
394
+ ClientId: str = Field(description="应用的 Client ID (原 AppKey 和 SuiteKey)")
395
+ ClientSecret: str = Field(description="应用的 Client Secret (原 AppSecret 和 SuiteSecret)")
396
+
397
+ @classmethod
398
+ def _is_dagster_maintained(cls) -> bool:
399
+ return False
400
+
401
+ def create_resource(self, context: InitResourceContext) -> DingTalkClient:
402
+ """
403
+ 返回一个 `DingTalkClient` 实例。
404
+ :param context:
405
+ :return:
406
+ """
407
+ return DingTalkClient(
408
+ app_id=self.AppID,
409
+ agent_id=self.AgentID,
410
+ app_name=self.AppName,
411
+ client_id=self.ClientId,
412
+ client_secret=self.ClientSecret
413
+ )
@@ -0,0 +1 @@
1
+ __version__ = "0.1.12"
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "dagster-dingtalk"
3
- version = "0.1.10b2"
3
+ version = "0.1.12"
4
4
  description = "A dagster plugin for the DingTalk"
5
5
  authors = ["YiZixuan <sqkkyzx@qq.com>"]
6
6
  readme = "README.md"
@@ -1,155 +0,0 @@
1
- import logging
2
- import pickle
3
- import time
4
- from datetime import datetime
5
- from enum import Enum
6
- from pathlib import Path
7
- from typing import List, Literal, Dict, Tuple
8
- from httpx import Client
9
-
10
-
11
- # noinspection NonAsciiCharacters
12
- class DingTalkClient:
13
- def __init__(self, app_id: str, client_id: str, client_secret: str, app_name: str|None = None, agent_id: int|None = None):
14
- self.app_id: str = app_id
15
- self.app_name: str|None = app_name
16
- self.agent_id: int|None = agent_id
17
- self.client_id: str = client_id
18
- self.client_secret: str = client_secret
19
- self.robot_code: str = client_id
20
-
21
- access_token: str = self._get_access_token()
22
- self.api: Client = Client(base_url="https://api.dingtalk.com/", headers={"x-acs-dingtalk-access-token": access_token})
23
- self.oapi: Client = Client(base_url="https://oapi.dingtalk.com/", params={"access_token": access_token})
24
-
25
- self.智能人事 = 智能人事_API(self)
26
- self.通讯录管理 = 通讯录管理_API(self)
27
- self.文档文件 = 文档文件_API(self)
28
-
29
- def _get_access_token(self) -> str:
30
- access_token_cache = Path("/tmp/.dingtalk_cache")
31
-
32
- try:
33
- with open(access_token_cache, 'rb') as f:
34
- all_access_token: Dict[str, Tuple[str, int]] = pickle.loads(f.read())
35
- access_token, expire_in = all_access_token.get(self.app_id, ('', 0))
36
- except Exception as e:
37
- print(e)
38
- all_access_token = {}
39
- access_token, expire_in = all_access_token.get(self.app_id, ('', 0))
40
-
41
- if access_token and expire_in < int(time.time()):
42
- return access_token
43
- else:
44
- logging.warning(f"应用{self.app_name}<{self.app_id}> 鉴权缓存过期或不存在,正在重新获取...")
45
- response = Client().post(
46
- url="https://api.dingtalk.com/v1.0/oauth2/accessToken",
47
- json={"appKey": self.client_id, "appSecret": self.client_secret},
48
- )
49
- access_token:str = response.json().get("accessToken")
50
- expire_in:int = response.json().get("expireIn") + int(time.time()) - 60
51
- with open(access_token_cache, 'wb') as f:
52
- all_access_token[self.app_id] = (access_token, expire_in)
53
- f.write(pickle.dumps(all_access_token))
54
- return access_token
55
-
56
-
57
- # noinspection NonAsciiCharacters
58
- class 智能人事_API:
59
- def __init__(self, _client:DingTalkClient):
60
- self.花名册 = 智能人事_花名册_API(_client)
61
- self.员工管理 = 智能人事_员工管理_API(_client)
62
-
63
- # noinspection NonAsciiCharacters
64
- class 智能人事_花名册_API:
65
- def __init__(self, _client:DingTalkClient):
66
- self.__client:DingTalkClient = _client
67
-
68
- def 获取花名册元数据(self) -> dict:
69
- response = self.__client.oapi.post(
70
- url="/topapi/smartwork/hrm/roster/meta/get",
71
- json={"agentid": self.__client.agent_id},
72
- )
73
- return response.json()
74
-
75
- def 获取员工花名册字段信息(self, user_id_list:List[str], field_filter_list:List[str]|None = None, text_to_select_convert:bool|None = None) -> dict:
76
- body_dict = {"userIdList": user_id_list, "appAgentId": self.__client.agent_id}
77
- if field_filter_list is not None:
78
- body_dict["fieldFilterList"] = field_filter_list
79
- if text_to_select_convert is not None:
80
- body_dict["text2SelectConvert"] = text_to_select_convert
81
-
82
- response = self.__client.api.post(url="/topapi/smartwork/hrm/roster/meta/get", json=body_dict, )
83
- return response.json()
84
-
85
- # noinspection NonAsciiCharacters
86
- class 智能人事_员工管理_API:
87
-
88
- def __init__(self, _client:DingTalkClient):
89
- self.__client:DingTalkClient = _client
90
-
91
- # noinspection NonAsciiCharacters
92
- class 在职员工状态(Enum):
93
- 试用期: '2'
94
- 正式: '3'
95
- 待离职: '5'
96
- 无状态: '-1'
97
-
98
- def 获取待入职员工列表(self, offset:int, size:int) -> dict:
99
- response = self.__client.oapi.post(
100
- "/topapi/smartwork/hrm/employee/querypreentry",
101
- json={"offset": offset, "size": size},
102
- )
103
- return response.json()
104
-
105
- def 获取在职员工列表(self, status_list:List[在职员工状态], offset:int, size:int) -> dict:
106
- response = self.__client.oapi.post(
107
- "/topapi/smartwork/hrm/employee/querypreentry",
108
- json={"status_list": status_list, "offset": offset, "size": size},
109
- )
110
- return response.json()
111
-
112
- def 获取离职员工列表(self, next_token:int, max_results:int) -> dict:
113
- response = self.__client.api.get(
114
- "/v1.0/hrm/employees/dismissions",
115
- params={"nextToken": next_token, "maxResults": max_results},
116
- )
117
- return response.json()
118
-
119
- def 批量获取员工离职信息(self, user_id_list:List[str]) -> dict:
120
- response = self.__client.api.get(
121
- "/v1.0/hrm/employees/dimissionInfo",
122
- params={"userIdList": user_id_list},
123
- )
124
- return response.json()
125
-
126
- # noinspection NonAsciiCharacters
127
- class 通讯录管理_API:
128
- def __init__(self, _client:DingTalkClient):
129
- self.__client = _client
130
-
131
- def 查询用户详情(self, user_id:str, language:str = "zh_CN") -> dict:
132
- response = self.__client.oapi.post(url="/topapi/v2/user/get", json={"language": language, "userid": user_id})
133
- return response.json()
134
-
135
- def 查询离职记录列表(self, start_time:datetime, end_time:datetime|None, next_token:str, max_results:int) -> dict:
136
- params = {"startTime": start_time.strftime("%Y-%m-%dT%H:%M:%SZ"), "nextToken": next_token, "maxResults": max_results}
137
- if end_time is not None:
138
- params["endTime"] = end_time.strftime("%Y-%m-%dT%H:%M:%SZ")
139
- response = self.__client.api.get(url="/v1.0/contact/empLeaveRecords", params=params)
140
- return response.json()
141
-
142
- # noinspection NonAsciiCharacters
143
- class 文档文件_API:
144
- def __init__(self, _client:DingTalkClient):
145
- self.媒体文件 = 文档文件_媒体文件_API(_client)
146
-
147
- # noinspection NonAsciiCharacters
148
- class 文档文件_媒体文件_API:
149
- def __init__(self, _client:DingTalkClient):
150
- self.__client = _client
151
-
152
- def 上传媒体文件(self, file_path:Path|str, media_type:Literal['image', 'voice', 'video', 'file']) -> dict:
153
- with open(file_path, 'rb') as f:
154
- response = self.__client.oapi.post(url=f"/media/upload?type={media_type}", files={'media': f})
155
- return response.json()
@@ -1,221 +0,0 @@
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
- class DingTalkWebhookResource(ConfigurableResource):
15
- """
16
- 定义一个钉钉群 Webhook 机器人资源,可以用来发送各类通知。
17
-
18
- 钉钉API文档:
19
- https://open.dingtalk.com/document/orgapp/custom-bot-send-message-type
20
-
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
-
27
- """
28
- access_token: str = Field(description="Webhook地址中的 access_token 部分")
29
- secret: Optional[str] = Field(default=None, description="如使用加签安全配置,需传签名密钥")
30
- alias: Optional[str] = Field(default=None, description="如提供别名,将来可以使用别名进行选择")
31
- base_url: str = Field(default="https://oapi.dingtalk.com/robot/send", description="Webhook的通用地址,无需更改")
32
-
33
- def webhook_url(self):
34
- """
35
- 实时生成加签或未加签的 Webhook URL。
36
-
37
- 钉钉API文档:
38
- https://open.dingtalk.com/document/robots/custom-robot-access
39
-
40
- Returns:
41
- str: Webhook URL
42
- """
43
- if self.secret is None:
44
- return f"{self.base_url}?access_token={self.access_token}"
45
- else:
46
- timestamp = round(time.time() * 1000)
47
- hmac_code = hmac.new(
48
- self.secret.encode('utf-8'), f'{timestamp}\n{self.secret}'.encode('utf-8'), digestmod=hashlib.sha256
49
- ).digest()
50
- sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))
51
- return f"{self.base_url}?access_token={self.access_token}&timestamp={timestamp}&sign={sign}"
52
-
53
- @staticmethod
54
- def _gen_title(text):
55
- """
56
- 从文本截取前12个字符作为标题,并清理其中的 Markdown 格式字符。
57
-
58
- Args:
59
- text: 原文
60
-
61
- Returns:
62
- str: 标题
63
- """
64
- return re.sub(r'[\n#>* ]', '', text[:12])
65
-
66
- def send_text(self, text: str,
67
- at_mobiles:List[str]|None = None, at_user_ids:List[str]|None = None, at_all:bool = False):
68
- """
69
- 发送文本消息。
70
-
71
- 钉钉API文档:
72
- https://open.dingtalk.com/document/orgapp/custom-bot-send-message-type
73
-
74
- Args:
75
- text (str): 待发送文本
76
- at_mobiles (List[str], optional): 需要 @ 的用户手机号。默认值为 None
77
- at_user_ids (List[str], optional): 需要 @ 的用户 UserID。默认值为 None
78
- at_all (bool, optional): 是否 @ 所有人。默认值为 False
79
- """
80
- at = {"isAtAll": at_all}
81
- if at_user_ids:
82
- at["atUserIds"] = at_user_ids
83
- if at_mobiles:
84
- at["atMobiles"] = at_mobiles
85
- httpx.post(url=self.webhook_url(), json={"msgtype": "text", "text": {"content": text}, "at": at})
86
-
87
- def send_link(self, text: str, message_url:str, title:str|None = None, pic_url:str = ""):
88
- """
89
- 发送 Link 消息。
90
-
91
- 钉钉API文档:
92
- https://open.dingtalk.com/document/orgapp/custom-bot-send-message-type
93
-
94
- Args:
95
- text (str): 待发送文本
96
- message_url (str): 链接的 Url
97
- title (str, optional): 标题,在通知和被引用时显示的简短信息。默认从文本中生成。
98
- pic_url (str, optional): 图片的 Url,默认为 None
99
- """
100
- title = title or self._gen_title(text)
101
- httpx.post(
102
- url=self.webhook_url(),
103
- json={"msgtype": "link", "link": {"title": title, "text": text, "picUrl": pic_url, "messageUrl": message_url}}
104
- )
105
-
106
- def send_markdown(self, text: List[str]|str, title:str|None = None,
107
- at_mobiles:List[str]|None = None, at_user_ids:List[str]|None = None, at_all:bool = False):
108
- """
109
- 发送 Markdown 消息。支持的语法有:
110
- # 一级标题
111
- ## 二级标题
112
- ### 三级标题
113
- #### 四级标题
114
- ##### 五级标题
115
- ###### 六级标题
116
- > 引用
117
- **加粗**
118
- *斜体*
119
- [链接跳转](https://example.com/doc.html)
120
- ![图片预览](https://example.com/pic.jpg)
121
- - 无序列表
122
- 1. 有序列表
123
-
124
- 钉钉API文档:
125
- https://open.dingtalk.com/document/orgapp/custom-bot-send-message-type
126
-
127
- Args:
128
- text (str): 待发送文本
129
- title (str, optional): 标题,在通知和被引用时显示的简短信息。默认从文本中生成。
130
- at_mobiles (List[str], optional): 需要 @ 的用户手机号。默认值为 None
131
- at_user_ids (List[str], optional): 需要 @ 的用户 UserID。默认值为 None
132
- at_all (bool, optional): 是否 @ 所有人。默认值为 False
133
- """
134
- text = text if isinstance(text, str) else "\n\n".join(text)
135
- title = title or self._gen_title(text)
136
- at = {"isAtAll": at_all}
137
- if at_user_ids:
138
- at["atUserIds"] = at_user_ids
139
- if at_mobiles:
140
- at["atMobiles"] = at_mobiles
141
- httpx.post(url=self.webhook_url(),json={"msgtype": "markdown", "markdown": {"title": title, "text": text}, "at": at})
142
-
143
- def send_action_card(self, text: List[str]|str, title:str|None = None, btn_orientation:Literal["0","1"] = "0",
144
- single_jump:Tuple[str,str]|None = None, btns_jump:List[Tuple[str,str]]|None = None):
145
- """
146
- 发送跳转 ActionCard 消息。
147
-
148
- Args:
149
- text (str): 待发送文本,支持 Markdown 部分语法。
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): 传此参数为多个按钮,元组内第一项为按钮的标题,第二项为按钮链接。
154
-
155
- Notes:
156
- 同时传 single_jump 和 btns_jump,仅 single_jump 生效。
157
-
158
- """
159
- text = text if isinstance(text, str) else "\n\n".join(text)
160
- title = title or self._gen_title(text)
161
- action_card = {"title": title, "text": text, "btnOrientation": str(btn_orientation)}
162
- if single_jump:
163
- action_card["singleTitle"], action_card["singleURL"] = single_jump
164
- httpx.post(url=self.webhook_url(), json={"msgtype": "actionCard", "actionCard": action_card})
165
- elif btns_jump:
166
- action_card["btns"] = [{"title": action_title, "actionURL": action_url} for action_title, action_url in btns_jump]
167
- httpx.post(url=self.webhook_url(), json={"msgtype": "actionCard", "actionCard": action_card})
168
- else:
169
- pass
170
-
171
- def send_feed_card(self, *args:Tuple[str,str,str]):
172
- """
173
- 发送 FeedCard 消息。
174
-
175
- Args:
176
- args (Tuple[str,str,str]): 可以传入任意个具有三个元素的元组,分别为 (标题, 跳转链接, 缩略图链接)
177
-
178
- """
179
- for a in args:
180
- print(a)
181
- links_data = [
182
- {"title": title, "messageURL": message_url, "picURL": pic_url}
183
- for title, message_url, pic_url in args
184
- ]
185
- httpx.post(url=self.webhook_url(), json={"msgtype": "feedCard", "feedCard": {"links": links_data}})
186
-
187
-
188
- class DingTalkAppResource(ConfigurableResource):
189
- """
190
- [钉钉服务端 API](https://open.dingtalk.com/document/orgapp/api-overview) 企业内部应用部分的第三方封装。
191
- 通过此资源,可以调用部分钉钉服务端API。
192
-
193
- Notes:
194
- 不包含全部的API端点。
195
-
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
- """
203
-
204
- AppID: str = Field(description="应用应用唯一标识 AppID,作为缓存标识符使用。不传入则不缓存鉴权。")
205
- AgentID: Optional[int] = Field(default=None, description="原企业内部应用AgentId ,部分API会使用到。")
206
- AppName: Optional[str] = Field(default=None, description="应用名。")
207
- ClientId: str = Field(description="应用的 Client ID (原 AppKey 和 SuiteKey)")
208
- ClientSecret: str = Field(description="应用的 Client Secret (原 AppSecret 和 SuiteSecret)")
209
-
210
- @classmethod
211
- def _is_dagster_maintained(cls) -> bool:
212
- return False
213
-
214
- def create_resource(self, context: InitResourceContext) -> DingTalkClient:
215
- return DingTalkClient(
216
- app_id=self.AppID,
217
- agent_id=self.AgentID,
218
- app_name=self.AppName,
219
- client_id=self.ClientId,
220
- client_secret=self.ClientSecret
221
- )
@@ -1 +0,0 @@
1
- __version__ = "0.1.10b2"