dagster-dingtalk 0.1.10b1__tar.gz → 0.1.11__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.10b1
3
+ Version: 0.1.11
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,334 @@
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
+ 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}>"
61
+
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:
68
+ return access_token
69
+ else:
70
+ logging.warning(renew_reason)
71
+ response = Client().post(
72
+ url="https://api.dingtalk.com/v1.0/oauth2/accessToken",
73
+ json={"appKey": self.client_id, "appSecret": self.__client_secret},
74
+ )
75
+ access_token:str = response.json().get("accessToken")
76
+ expire_in:int = response.json().get("expireIn") + int(time.time()) - 60
77
+ with open(access_token_cache, 'wb') as f:
78
+ all_access_token[self.app_id] = {
79
+ "access_token": access_token,
80
+ "expire_in": expire_in,
81
+ }
82
+ f.write(pickle.dumps(all_access_token))
83
+ return access_token
84
+
85
+ # noinspection NonAsciiCharacters
86
+ class 智能人事_API:
87
+ def __init__(self, _client:DingTalkClient):
88
+ self.__client:DingTalkClient = _client
89
+ self.花名册 = 智能人事_花名册_API(_client)
90
+ self.员工管理 = 智能人事_员工管理_API(_client)
91
+
92
+ # noinspection NonAsciiCharacters
93
+ class 智能人事_花名册_API:
94
+ def __init__(self, _client:DingTalkClient):
95
+ self.__client:DingTalkClient = _client
96
+
97
+ def 获取花名册元数据(self) -> dict:
98
+ response = self.__client.oapi.post(
99
+ url="/topapi/smartwork/hrm/roster/meta/get",
100
+ json={"agentid": self.__client.agent_id},
101
+ )
102
+ return response.json()
103
+
104
+ def 获取员工花名册字段信息(self, user_id_list:List[str], field_filter_list:List[str]|None = None, text_to_select_convert:bool|None = None) -> dict:
105
+ body_dict = {"userIdList": user_id_list, "appAgentId": self.__client.agent_id}
106
+ if field_filter_list is not None:
107
+ body_dict["fieldFilterList"] = field_filter_list
108
+ if text_to_select_convert is not None:
109
+ body_dict["text2SelectConvert"] = text_to_select_convert
110
+
111
+ response = self.__client.api.post(url="/topapi/smartwork/hrm/roster/meta/get", json=body_dict, )
112
+ return response.json()
113
+
114
+ # noinspection NonAsciiCharacters
115
+ class 智能人事_员工管理_API:
116
+ def __init__(self, _client:DingTalkClient):
117
+ self.__client:DingTalkClient = _client
118
+
119
+ # noinspection NonAsciiCharacters
120
+ class 在职员工状态(Enum):
121
+ 试用期: '2'
122
+ 正式: '3'
123
+ 待离职: '5'
124
+ 无状态: '-1'
125
+
126
+ def 获取待入职员工列表(self, offset:int, size:int) -> dict:
127
+ response = self.__client.oapi.post(
128
+ "/topapi/smartwork/hrm/employee/querypreentry",
129
+ json={"offset": offset, "size": size},
130
+ )
131
+ return response.json()
132
+
133
+ def 获取在职员工列表(self, status_list:List[在职员工状态], offset:int, size:int) -> dict:
134
+ response = self.__client.oapi.post(
135
+ "/topapi/smartwork/hrm/employee/querypreentry",
136
+ json={"status_list": status_list, "offset": offset, "size": size},
137
+ )
138
+ return response.json()
139
+
140
+ def 获取离职员工列表(self, next_token:int, max_results:int) -> dict:
141
+ response = self.__client.api.get(
142
+ "/v1.0/hrm/employees/dismissions",
143
+ params={"nextToken": next_token, "maxResults": max_results},
144
+ )
145
+ return response.json()
146
+
147
+ def 批量获取员工离职信息(self, user_id_list:List[str]) -> dict:
148
+ response = self.__client.api.get(
149
+ "/v1.0/hrm/employees/dimissionInfo",
150
+ params={"userIdList": user_id_list},
151
+ )
152
+ return response.json()
153
+
154
+ # noinspection NonAsciiCharacters
155
+ class 通讯录管理_API:
156
+ def __init__(self, _client:DingTalkClient):
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
168
+
169
+ def 查询用户详情(self, user_id:str, language:str = "zh_CN") -> dict:
170
+ response = self.__client.oapi.post(url="/topapi/v2/user/get", json={"language": language, "userid": user_id})
171
+ return response.json()
172
+
173
+ def 查询离职记录列表(self, start_time:datetime, end_time:datetime|None, next_token:str, max_results:int) -> dict:
174
+ params = {"startTime": start_time.strftime("%Y-%m-%dT%H:%M:%SZ"), "nextToken": next_token, "maxResults": max_results}
175
+ if end_time is not None:
176
+ params["endTime"] = end_time.strftime("%Y-%m-%dT%H:%M:%SZ")
177
+ response = self.__client.api.get(url="/v1.0/contact/empLeaveRecords", params=params)
178
+ return response.json()
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
+
215
+ # noinspection NonAsciiCharacters
216
+ class 文档文件_API:
217
+ def __init__(self, _client:DingTalkClient):
218
+ self.__client:DingTalkClient = _client
219
+ self.媒体文件 = 文档文件_媒体文件_API(_client)
220
+
221
+ # noinspection NonAsciiCharacters
222
+ class 文档文件_媒体文件_API:
223
+ def __init__(self, _client:DingTalkClient):
224
+ self.__client:DingTalkClient = _client
225
+
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
+ """
244
+ with open(file_path, 'rb') as f:
245
+ response = self.__client.oapi.post(url=f"/media/upload?type={media_type}", files={'media': f})
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()
@@ -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.11"
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "dagster-dingtalk"
3
- version = "0.1.10b1"
3
+ version = "0.1.11"
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.10b1"