dagster-dingtalk 0.1.10b2__py3-none-any.whl → 0.1.11__py3-none-any.whl

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