dagster-dingtalk 0.1.8__py3-none-any.whl → 0.1.10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,9 @@
1
1
  # noinspection PyProtectedMember
2
2
  from dagster._core.libraries import DagsterLibraryRegistry
3
3
 
4
- from dagster_dingtalk.resources import DingTalkResource, MultiDingTalkResource
5
- from dagster_dingtalk.resources import DingTalkWebhookResource, MultiDingTalkWebhookResource
4
+ from dagster_dingtalk.resources import DingTalkAppResource
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,154 @@
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
+ # noinspection NonAsciiCharacters
57
+ class 智能人事_API:
58
+ def __init__(self, _client:DingTalkClient):
59
+ self.花名册 = 智能人事_花名册_API(_client)
60
+ self.员工管理 = 智能人事_员工管理_API(_client)
61
+
62
+ # noinspection NonAsciiCharacters
63
+ class 智能人事_花名册_API:
64
+ def __init__(self, _client:DingTalkClient):
65
+ self.__client:DingTalkClient = _client
66
+
67
+ def 获取花名册元数据(self) -> dict:
68
+ response = self.__client.oapi.post(
69
+ url="/topapi/smartwork/hrm/roster/meta/get",
70
+ json={"agentid": self.__client.agent_id},
71
+ )
72
+ return response.json()
73
+
74
+ def 获取员工花名册字段信息(self, user_id_list:List[str], field_filter_list:List[str]|None = None, text_to_select_convert:bool|None = None) -> dict:
75
+ body_dict = {"userIdList": user_id_list, "appAgentId": self.__client.agent_id}
76
+ if field_filter_list is not None:
77
+ body_dict["fieldFilterList"] = field_filter_list
78
+ if text_to_select_convert is not None:
79
+ body_dict["text2SelectConvert"] = text_to_select_convert
80
+
81
+ response = self.__client.api.post(url="/topapi/smartwork/hrm/roster/meta/get", json=body_dict, )
82
+ return response.json()
83
+
84
+ # noinspection NonAsciiCharacters
85
+ class 智能人事_员工管理_API:
86
+
87
+ def __init__(self, _client:DingTalkClient):
88
+ self.__client:DingTalkClient = _client
89
+
90
+ # noinspection NonAsciiCharacters
91
+ class 在职员工状态(Enum):
92
+ 试用期: '2'
93
+ 正式: '3'
94
+ 待离职: '5'
95
+ 无状态: '-1'
96
+
97
+ def 获取待入职员工列表(self, offset:int, size:int) -> dict:
98
+ response = self.__client.oapi.post(
99
+ "/topapi/smartwork/hrm/employee/querypreentry",
100
+ json={"offset": offset, "size": size},
101
+ )
102
+ return response.json()
103
+
104
+ def 获取在职员工列表(self, status_list:List[在职员工状态], offset:int, size:int) -> dict:
105
+ response = self.__client.oapi.post(
106
+ "/topapi/smartwork/hrm/employee/querypreentry",
107
+ json={"status_list": status_list, "offset": offset, "size": size},
108
+ )
109
+ return response.json()
110
+
111
+ def 获取离职员工列表(self, next_token:int, max_results:int) -> dict:
112
+ response = self.__client.api.get(
113
+ "/v1.0/hrm/employees/dismissions",
114
+ params={"nextToken": next_token, "maxResults": max_results},
115
+ )
116
+ return response.json()
117
+
118
+ def 批量获取员工离职信息(self, user_id_list:List[str]) -> dict:
119
+ response = self.__client.api.get(
120
+ "/v1.0/hrm/employees/dimissionInfo",
121
+ params={"userIdList": user_id_list},
122
+ )
123
+ return response.json()
124
+
125
+ # noinspection NonAsciiCharacters
126
+ class 通讯录管理_API:
127
+ def __init__(self, _client:DingTalkClient):
128
+ self.__client = _client
129
+
130
+ def 查询用户详情(self, user_id:str, language:str = "zh_CN") -> dict:
131
+ response = self.__client.oapi.post(url="/topapi/v2/user/get", json={"language": language, "userid": user_id})
132
+ return response.json()
133
+
134
+ def 查询离职记录列表(self, start_time:datetime, end_time:datetime|None, next_token:str, max_results:int) -> dict:
135
+ params = {"startTime": start_time.strftime("%Y-%m-%dT%H:%M:%SZ"), "nextToken": next_token, "maxResults": max_results}
136
+ if end_time is not None:
137
+ params["endTime"] = end_time.strftime("%Y-%m-%dT%H:%M:%SZ")
138
+ response = self.__client.api.get(url="/v1.0/contact/empLeaveRecords", params=params)
139
+ return response.json()
140
+
141
+ # noinspection NonAsciiCharacters
142
+ class 文档文件_API:
143
+ def __init__(self, _client:DingTalkClient):
144
+ self.媒体文件 = 文档文件_媒体文件_API(_client)
145
+
146
+ # noinspection NonAsciiCharacters
147
+ class 文档文件_媒体文件_API:
148
+ def __init__(self, _client:DingTalkClient):
149
+ self.__client = _client
150
+
151
+ def 上传媒体文件(self, file_path:Path|str, media_type:Literal['image', 'voice', 'video', 'file']) -> dict:
152
+ with open(file_path, 'rb') as f:
153
+ response = self.__client.oapi.post(url=f"/media/upload?type={media_type}", files={'media': f})
154
+ return response.json()
@@ -1,21 +1,20 @@
1
1
  from dagster import In, OpExecutionContext, op
2
2
 
3
+ # noinspection PyProtectedMember
4
+ from dagster._annotations import experimental
3
5
 
4
- class DingTalkWebhookOp:
5
- @staticmethod
6
- @op(description="使用钉钉 Webhook 发送文本消息",
7
- required_resource_keys={'dingtalk_webhook'},
8
- ins={"text": In(str)},
9
- )
10
- def send_simple_text(context: OpExecutionContext, text):
11
- webhook = context.resources.dingtalk_webhook
12
- webhook.send_text(text)
13
6
 
14
- @staticmethod
15
- @op(description="使用钉钉 Webhook 发送 Markdown 消息",
16
- required_resource_keys={'dingtalk_webhook'},
17
- ins={"text": In(str), "title": In(str, default_value='')},
18
- )
19
- def send_simple_markdown(context: OpExecutionContext, text, title):
20
- webhook = context.resources.dingtalk_webhook
21
- webhook.send_text(text, title)
7
+ @experimental
8
+ @op(description="使用钉钉 Webhook 发送文本消息",
9
+ required_resource_keys={'dingtalk_webhook'},
10
+ ins={"text": In(str)})
11
+ def op_send_simple_text(context: OpExecutionContext, text):
12
+ webhook = context.resources.dingtalk_webhook
13
+ webhook.send_text(text)
14
+
15
+ @op(description="使用钉钉 Webhook 发送 Markdown 消息",
16
+ required_resource_keys={'dingtalk_webhook'},
17
+ ins={"text": In(str), "title": In(str, default_value='')})
18
+ def op_simple_markdown(context: OpExecutionContext, text, title):
19
+ webhook = context.resources.dingtalk_webhook
20
+ webhook.send_text(text, title)
@@ -1,30 +1,45 @@
1
1
  import base64
2
2
  import hashlib
3
3
  import hmac
4
+ import re
4
5
  import time
5
6
  import urllib.parse
6
- from datetime import datetime
7
- from enum import Enum
8
- from pathlib import Path
9
- from typing import Optional, Dict, Tuple, List, Literal
10
- import pickle
7
+ from typing import Optional, Tuple, List, Literal
11
8
  import httpx
12
-
13
- from dagster import (
14
- ConfigurableResource,
15
- InitResourceContext, ResourceDependency,
16
- )
17
- from httpx import Client
18
- from pydantic import Field, PrivateAttr
9
+ from pydantic import Field
10
+ from .app_client import DingTalkClient
11
+ from dagster import ConfigurableResource, InitResourceContext
19
12
 
20
13
 
21
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
+ """
22
28
  access_token: str = Field(description="Webhook地址中的 access_token 部分")
23
29
  secret: Optional[str] = Field(default=None, description="如使用加签安全配置,需传签名密钥")
24
30
  alias: Optional[str] = Field(default=None, description="如提供别名,将来可以使用别名进行选择")
25
31
  base_url: str = Field(default="https://oapi.dingtalk.com/robot/send", description="Webhook的通用地址,无需更改")
26
32
 
27
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
+ """
28
43
  if self.secret is None:
29
44
  return f"{self.base_url}?access_token={self.access_token}"
30
45
  else:
@@ -37,10 +52,31 @@ class DingTalkWebhookResource(ConfigurableResource):
37
52
 
38
53
  @staticmethod
39
54
  def _gen_title(text):
40
- return text[:12].replace("\n", "").replace("#", "").replace(">", "").replace("*", "")
55
+ """
56
+ 从文本截取前12个字符作为标题,并清理其中的 Markdown 格式字符。
57
+
58
+ Args:
59
+ text: 原文
60
+
61
+ Returns:
62
+ str: 标题
63
+ """
64
+ return re.sub(r'[\n#>* ]', '', text[:12])
41
65
 
42
66
  def send_text(self, text: str,
43
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
+ """
44
80
  at = {"isAtAll": at_all}
45
81
  if at_user_ids:
46
82
  at["atUserIds"] = at_user_ids
@@ -49,6 +85,18 @@ class DingTalkWebhookResource(ConfigurableResource):
49
85
  httpx.post(url=self.webhook_url(), json={"msgtype": "text", "text": {"content": text}, "at": at})
50
86
 
51
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
+ """
52
100
  title = title or self._gen_title(text)
53
101
  httpx.post(
54
102
  url=self.webhook_url(),
@@ -57,6 +105,32 @@ class DingTalkWebhookResource(ConfigurableResource):
57
105
 
58
106
  def send_markdown(self, text: List[str]|str, title:str|None = None,
59
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
+ """
60
134
  text = text if isinstance(text, str) else "\n\n".join(text)
61
135
  title = title or self._gen_title(text)
62
136
  at = {"isAtAll": at_all}
@@ -68,99 +142,63 @@ class DingTalkWebhookResource(ConfigurableResource):
68
142
 
69
143
  def send_action_card(self, text: List[str]|str, title:str|None = None, btn_orientation:Literal["0","1"] = "0",
70
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
+ """
71
159
  text = text if isinstance(text, str) else "\n\n".join(text)
72
160
  title = title or self._gen_title(text)
73
- action_card = {"title": title, "text": text, "btnOrientation": btn_orientation}
74
- if btns_jump:
75
- action_card["btns"] = [{"title": action_title, "actionURL": action_url} for action_title, action_url in btns_jump]
76
- httpx.post(url=self.webhook_url(), json={"msgtype": "actionCard", "actionCard": action_card})
77
- elif single_jump:
161
+ action_card = {"title": title, "text": text, "btnOrientation": str(btn_orientation)}
162
+ if single_jump:
78
163
  action_card["singleTitle"], action_card["singleURL"] = single_jump
79
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})
80
168
  else:
81
169
  pass
82
170
 
83
- def send_feed_card(self, links: List[Tuple[str,str,str]]):
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)
84
181
  links_data = [
85
182
  {"title": title, "messageURL": message_url, "picURL": pic_url}
86
- for title, message_url, pic_url in links
183
+ for title, message_url, pic_url in args
87
184
  ]
88
185
  httpx.post(url=self.webhook_url(), json={"msgtype": "feedCard", "feedCard": {"links": links_data}})
89
186
 
90
187
 
91
- # noinspection NonAsciiCharacters
92
- class MultiDingTalkWebhookResource(ConfigurableResource):
93
- """
94
- 该资源提供了预先定义多个 webhook 资源,并在运行时动态选择的方法。
95
-
96
- 使用示例:
97
-
98
- ```
99
- from dagster_dingtalk import DingTalkResource, MultiDingTalkResource
100
-
101
- app_apple = DingTalkResource(AppID="apple", ClientId="", ClientSecret="")
102
- app_book = DingTalkResource(AppID="book", ClientId="", ClientSecret="")
103
-
104
- @op(required_resource_keys={"dingtalk"}, ins={"app_id":In(str)})
105
- def print_app_id(context:OpExecutionContext, app_id):
106
- dingtalk:DingTalkResource = context.resources.dingtalk
107
- select_app = dingtalk.select_app(app_id)
108
- context.log.info(dingtalk_app.AppName)
109
-
110
- @job
111
- def print_app_id_job():
112
- print_app_id()
113
-
114
- defs = Definitions(
115
- jobs=[print_app_id_job],
116
- resources={
117
- "dingtalk": MultiDingTalkResource(
118
- Apps=[app_apple, app_book]
119
- )
120
- },
121
- )
122
- ```
123
-
124
- """
125
-
126
- Webhooks: ResourceDependency[List[DingTalkWebhookResource]] = Field(description="多个 Webhook 资源的列表")
127
-
128
- _webhooks = PrivateAttr()
129
-
130
- def setup_for_execution(self, context: InitResourceContext) -> None:
131
- _webhooks_token_key = {webhook.access_token:webhook for webhook in self.Webhooks}
132
- _webhooks_alias_key = {webhook.alias:webhook for webhook in self.Webhooks if webhook.alias}
133
- self._webhooks = _webhooks_token_key | _webhooks_alias_key
134
-
135
- def select(self, key:str = "_FIRST_"):
136
- try:
137
- if key == "_FIRST_" or key is None:
138
- webhook = self.Webhooks[0]
139
- else:
140
- webhook = self._webhooks[key]
141
- webhook.init_webhook_url()
142
- return webhook
143
- except KeyError:
144
- raise f"该 AccessToken 或 别名 <{key}> 不存在于提供的 Webhooks 中。请使用 DingTalkWebhookResource 定义单个 Webhook 后,将其加入 Webhooks 。"
145
-
146
-
147
- class DingTalkClient:
148
- def __init__(self, access_token: str, app_id: str, agent_id: int, robot_code: str) -> None:
149
- self.access_token: str = access_token
150
- self.app_id: str = app_id
151
- self.agent_id: int = agent_id
152
- self.robot_code: str = robot_code
153
- self.api: Client = httpx.Client(base_url="https://api.dingtalk.com/", headers={"x-acs-dingtalk-access-token": self.access_token})
154
- self.oapi: Client = httpx.Client(base_url="https://oapi.dingtalk.com/", params={"access_token": self.access_token})
155
-
156
-
157
- # noinspection NonAsciiCharacters
158
- class DingTalkResource(ConfigurableResource):
188
+ class DingTalkAppResource(ConfigurableResource):
159
189
  """
160
190
  [钉钉服务端 API](https://open.dingtalk.com/document/orgapp/api-overview) 企业内部应用部分的第三方封装。
161
191
  通过此资源,可以调用部分钉钉服务端API。
162
192
 
163
- 注意:不包含全部的API端点。
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
164
202
  """
165
203
 
166
204
  AppID: str = Field(description="应用应用唯一标识 AppID,作为缓存标识符使用。不传入则不缓存鉴权。")
@@ -168,203 +206,16 @@ class DingTalkResource(ConfigurableResource):
168
206
  AppName: Optional[str] = Field(default=None, description="应用名。")
169
207
  ClientId: str = Field(description="应用的 Client ID (原 AppKey 和 SuiteKey)")
170
208
  ClientSecret: str = Field(description="应用的 Client Secret (原 AppSecret 和 SuiteSecret)")
171
- RobotCode: Optional[str] = Field(default=None, description="应用的机器人 RobotCode,不传时使用 self.ClientId ")
172
-
173
- _client: DingTalkClient = PrivateAttr()
174
209
 
175
210
  @classmethod
176
211
  def _is_dagster_maintained(cls) -> bool:
177
212
  return False
178
213
 
179
- def _get_access_token(self) -> str:
180
- access_token_cache = Path("/tmp/.dingtalk_cache")
181
-
182
- try:
183
- with open(access_token_cache, 'rb') as f:
184
- all_access_token: Dict[str, Tuple[str, int]] = pickle.loads(f.read())
185
- access_token, expire_in = all_access_token.get(self.AppID, ('', 0))
186
- except Exception as e:
187
- print(e)
188
- all_access_token = {}
189
- access_token, expire_in = all_access_token.get(self.AppID, ('', 0))
190
-
191
- if access_token and expire_in < int(time.time()):
192
- return access_token
193
- else:
194
- print(f"应用{self.AppName}<{self.AppID}> 鉴权缓存过期或不存在,正在重新获取...")
195
- response = httpx.post(
196
- url="https://api.dingtalk.com/v1.0/oauth2/accessToken",
197
- json={"appKey": self.ClientId, "appSecret": self.ClientSecret},
198
- )
199
- access_token:str = response.json().get("accessToken")
200
- expire_in:int = response.json().get("expireIn") + int(time.time()) - 60
201
- with open(access_token_cache, 'wb') as f:
202
- all_access_token[self.AppID] = (access_token, expire_in)
203
- f.write(pickle.dumps(all_access_token))
204
- return access_token
205
-
206
- def init_client(self):
207
- if not hasattr(self, '_client'):
208
- self._client = DingTalkClient(
209
- self._get_access_token(),
210
- self.AppID,
211
- self.AgentID,
212
- self.RobotCode or self.ClientId
213
- )
214
-
215
- def setup_for_execution(self, context: InitResourceContext) -> None:
216
- self.init_client()
217
-
218
- def teardown_after_execution(self, context: InitResourceContext) -> None:
219
- self._client.api.close()
220
- self._client.oapi.close()
221
-
222
- def 智能人事(self):
223
- return API_智能人事(self._client)
224
-
225
- def 通讯录管理(self):
226
- return API_通讯录管理(self._client)
227
-
228
- def 文档文件(self):
229
- return API_文档文件(self._client)
230
-
231
-
232
- # noinspection NonAsciiCharacters
233
- class MultiDingTalkResource(ConfigurableResource):
234
- """
235
- 该资源提供了预先定义多个应用资源,并在运行时动态选择的方法。
236
-
237
- 使用示例:
238
-
239
- ```
240
- from dagster_dingtalk import DingTalkResource, MultiDingTalkResource
241
-
242
- app_apple = DingTalkResource(AppID="apple", ClientId="", ClientSecret="")
243
- app_book = DingTalkResource(AppID="book", ClientId="", ClientSecret="")
244
-
245
- @op(required_resource_keys={"dingtalk"}, ins={"app_id":In(str)})
246
- def print_app_id(context:OpExecutionContext, app_id):
247
- dingtalk:DingTalkResource = context.resources.dingtalk
248
- select_app = dingtalk.select_app(app_id)
249
- context.log.info(dingtalk_app.AppName)
250
-
251
- @job
252
- def print_app_id_job():
253
- print_app_id()
254
-
255
- defs = Definitions(
256
- jobs=[print_app_id_job],
257
- resources={
258
- "dingtalk": MultiDingTalkResource(
259
- Apps=[app_apple, app_book]
260
- )
261
- },
262
- )
263
- ```
264
-
265
- """
266
-
267
- Apps: ResourceDependency[List[DingTalkResource]] = Field(description="多个单应用资源的列表")
268
-
269
- _apps = PrivateAttr()
270
-
271
- def setup_for_execution(self, context: InitResourceContext) -> None:
272
- self._apps = {app.AppID:app for app in self.Apps}
273
-
274
- def select(self, app_id:str = "_FIRST_"):
275
- try:
276
- if app_id == "_FIRST_" or app_id is None:
277
- app = self.Apps[0]
278
- else:
279
- app = self._apps[app_id]
280
- app.init_client()
281
- return app
282
- except KeyError:
283
- raise f"该 AppID <{app_id}> 不存在于提供的 AppLists 中。请使用 DingTalkResource 定义单个 App 后,将其加入 AppLists 。"
284
-
285
-
286
- # noinspection NonAsciiCharacters
287
- class API_智能人事:
288
- def __init__(self, _client:DingTalkClient):
289
- self._client = _client
290
-
291
- def 花名册_获取花名册元数据(self) -> dict:
292
- response = self._client.oapi.post(
293
- url="/topapi/smartwork/hrm/roster/meta/get",
294
- json={"agentid": self._client.agent_id},
295
- )
296
- return response.json()
297
-
298
- def 花名册_获取员工花名册字段信息(self, user_id_list:List[str], field_filter_list:List[str]|None = None, text_to_select_convert:bool|None = None) -> dict:
299
- body_dict = {"userIdList": user_id_list, "appAgentId": self._client.agent_id}
300
- if field_filter_list is not None:
301
- body_dict["fieldFilterList"] = field_filter_list
302
- if text_to_select_convert is not None:
303
- body_dict["text2SelectConvert"] = text_to_select_convert
304
-
305
- response = self._client.api.post(url="/topapi/smartwork/hrm/roster/meta/get", json=body_dict,)
306
- return response.json()
307
-
308
- # noinspection NonAsciiCharacters
309
- class 在职员工状态(Enum):
310
- 试用期: '2'
311
- 正式: '3'
312
- 待离职: '5'
313
- 无状态: '-1'
314
-
315
- def 员工管理_获取待入职员工列表(self, offset:int, size:int) -> dict:
316
- response = self._client.oapi.post(
317
- "/topapi/smartwork/hrm/employee/querypreentry",
318
- json={"offset": offset, "size": size},
319
- )
320
- return response.json()
321
-
322
- def 员工管理_获取在职员工列表(self, status_list:List[在职员工状态], offset:int, size:int) -> dict:
323
- response = self._client.oapi.post(
324
- "/topapi/smartwork/hrm/employee/querypreentry",
325
- json={"status_list": status_list, "offset": offset, "size": size},
326
- )
327
- return response.json()
328
-
329
- def 员工管理_获取离职员工列表(self, next_token:int, max_results:int) -> dict:
330
- response = self._client.api.get(
331
- "/v1.0/hrm/employees/dismissions",
332
- params={"nextToken": next_token, "maxResults": max_results},
333
- )
334
- return response.json()
335
-
336
- def 员工管理_批量获取员工离职信息(self, user_id_list:List[str]) -> dict:
337
- response = self._client.api.get(
338
- "/v1.0/hrm/employees/dimissionInfo",
339
- params={"userIdList": user_id_list},
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
340
221
  )
341
- return response.json()
342
-
343
-
344
- # noinspection NonAsciiCharacters
345
- class API_通讯录管理:
346
- def __init__(self, _client:DingTalkClient):
347
- self._client = _client
348
-
349
- def 查询用户详情(self, user_id:str, language:str = "zh_CN") -> dict:
350
- response = self._client.oapi.post(url="/topapi/v2/user/get", json={"language": language, "userid": user_id})
351
- return response.json()
352
-
353
- def 查询离职记录列表(self, start_time:datetime, end_time:datetime|None, next_token:str, max_results:int) -> dict:
354
- params = {"startTime": start_time.strftime("%Y-%m-%dT%H:%M:%SZ"), "nextToken": next_token, "maxResults": max_results}
355
- if end_time is not None:
356
- params["endTime"] = end_time.strftime("%Y-%m-%dT%H:%M:%SZ")
357
- response = self._client.api.get(url="/v1.0/contact/empLeaveRecords", params=params)
358
- return response.json()
359
-
360
-
361
- # noinspection NonAsciiCharacters
362
- class API_文档文件:
363
- def __init__(self, _client:DingTalkClient):
364
- self._client = _client
365
-
366
- def 媒体文件_上传媒体文件(self, file_path:Path|str, media_type:Literal['image', 'voice', 'video', 'file']) -> dict:
367
- with open(file_path, 'rb') as f:
368
- response = self._client.oapi.post(url=f"/media/upload?type={media_type}", files={'media': f})
369
- return response.json()
370
-
@@ -1 +1 @@
1
- __version__ = "0.0.1"
1
+ __version__ = "0.1.10"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dagster-dingtalk
3
- Version: 0.1.8
3
+ Version: 0.1.10
4
4
  Summary: A dagster plugin for the DingTalk
5
5
  Author: YiZixuan
6
6
  Author-email: sqkkyzx@qq.com
@@ -20,28 +20,19 @@ Description-Content-Type: text/markdown
20
20
 
21
21
  ## 介绍
22
22
 
23
- 该 Dagster 集成是为了更便捷的调用钉钉(DingTalk)的API,集成提供了两个 Dagster
24
- Resource 和若干 Dagster Op 的封装。
23
+ 该 Dagster 集成是为了更便捷的调用钉钉(DingTalk)的API,集成提供了两个 Dagster Resource。
24
+
25
+
26
+ ## Webhook 资源
25
27
 
26
28
  ### DingTalkWebhookResource
27
29
 
28
- Dagster 资源允许定义一个钉钉自定义机器人的 Webhook 端点,发送文本、Markdown
30
+ 该资源允许定义单个钉钉自定义机器人的 Webhook 端点,以便于发送文本、Markdown
29
31
  、Link、 ActionCard、FeedCard 消息,消息具体样式可参考
30
32
  [钉钉开放平台 | 自定义机器人发送消息的消息类型](https://open.dingtalk.com/document/orgapp/custom-bot-send-message-type) 。
31
33
 
32
34
 
33
- ### DingTalkAPIResource
34
-
35
- 该 Dagster 资源允许定义一个钉钉的 API Client,更加便捷地调用钉钉服务端 API (
36
- 仅企业内部应用)
37
-
38
-
39
- ### DingTalkWebhookOp
40
-
41
- 该类提供一些预定义的 Dagster Op ,以便于快速调用。
35
+ ### DingTalkAppResource
42
36
 
43
- 需要注意的是,这些 Op 都没有静态声明 `required_resource_keys`,而是需要根据
44
- 上下文配置 `context.op_config` 中的 `dingtalk_webhook_key` 键来动态查找
45
- 资源。在使用时,必须在 Job 中提供 `resource_defs` ,或在 `User Code` 全局
46
- 定义会使用到的 `dingtalk_webhook_key`。
37
+ Dagster 资源允许定义一个钉钉的 API Client,更加便捷地调用钉钉服务端企业内部应用 API
47
38
 
@@ -0,0 +1,8 @@
1
+ dagster_dingtalk/__init__.py,sha256=X7r8JoydXOsT9Sis4rBpVSKQeKJnnZ_t_qFae-ASF7E,466
2
+ dagster_dingtalk/app_client.py,sha256=U4rEmfpG9J0rqoksWCZxQg6QkdHICwLtBzQL64MjvUY,6749
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=z0zCHFTcKSR0tJ6h5qrpNmRVP21QIPP8N0p7quCnnm0,23
6
+ dagster_dingtalk-0.1.10.dist-info/METADATA,sha256=4fjDXlG6sooFgoxCnbFOXlY0x2hBiEiBaMHRA4ojn_A,1221
7
+ dagster_dingtalk-0.1.10.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
8
+ dagster_dingtalk-0.1.10.dist-info/RECORD,,
@@ -1,7 +0,0 @@
1
- dagster_dingtalk/__init__.py,sha256=ktvoURpkJwIzcyQfUvnel1KA4DukRgavAgLl7f0Cy_0,440
2
- dagster_dingtalk/operations.py,sha256=xJJlOVmFjpaDTMkHZXxj5LbXqRtIQwREl9ZJdXIMOyE,788
3
- dagster_dingtalk/resources.py,sha256=iWhVHtct5DDKk3-ejlT1t_N0pFtfD_nb1h6xr4rVSSQ,15231
4
- dagster_dingtalk/version.py,sha256=sXLh7g3KC4QCFxcZGBTpG2scR7hmmBsMjq6LqRptkRg,22
5
- dagster_dingtalk-0.1.8.dist-info/METADATA,sha256=KlhB2moaofe0iHmVOFCgxtJ5AqEItCkbqFVkjaQKS2A,1659
6
- dagster_dingtalk-0.1.8.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
7
- dagster_dingtalk-0.1.8.dist-info/RECORD,,