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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,,