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.
- dagster_dingtalk/__init__.py +3 -2
- dagster_dingtalk/app_client.py +154 -0
- dagster_dingtalk/operations.py +16 -17
- dagster_dingtalk/resources.py +134 -283
- dagster_dingtalk/version.py +1 -1
- {dagster_dingtalk-0.1.8.dist-info → dagster_dingtalk-0.1.10.dist-info}/METADATA +8 -17
- dagster_dingtalk-0.1.10.dist-info/RECORD +8 -0
- dagster_dingtalk-0.1.8.dist-info/RECORD +0 -7
- {dagster_dingtalk-0.1.8.dist-info → dagster_dingtalk-0.1.10.dist-info}/WHEEL +0 -0
dagster_dingtalk/__init__.py
CHANGED
@@ -1,8 +1,9 @@
|
|
1
1
|
# noinspection PyProtectedMember
|
2
2
|
from dagster._core.libraries import DagsterLibraryRegistry
|
3
3
|
|
4
|
-
from dagster_dingtalk.resources import
|
5
|
-
from dagster_dingtalk.resources import DingTalkWebhookResource
|
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()
|
dagster_dingtalk/operations.py
CHANGED
@@ -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
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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)
|
dagster_dingtalk/resources.py
CHANGED
@@ -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
|
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
|
14
|
-
|
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
|
-
|
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
|
+

|
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
|
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,
|
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
|
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
|
-
|
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
|
-
|
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
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
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
|
-
|
dagster_dingtalk/version.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "0.
|
1
|
+
__version__ = "0.1.10"
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: dagster-dingtalk
|
3
|
-
Version: 0.1.
|
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
|
-
|
23
|
+
该 Dagster 集成是为了更便捷的调用钉钉(DingTalk)的API,集成提供了两个 Dagster Resource。
|
24
|
+
|
25
|
+
|
26
|
+
## Webhook 资源
|
25
27
|
|
26
28
|
### DingTalkWebhookResource
|
27
29
|
|
28
|
-
|
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
|
-
###
|
34
|
-
|
35
|
-
该 Dagster 资源允许定义一个钉钉的 API Client,更加便捷地调用钉钉服务端 API (
|
36
|
-
仅企业内部应用)
|
37
|
-
|
38
|
-
|
39
|
-
### DingTalkWebhookOp
|
40
|
-
|
41
|
-
该类提供一些预定义的 Dagster Op ,以便于快速调用。
|
35
|
+
### DingTalkAppResource
|
42
36
|
|
43
|
-
|
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,,
|
File without changes
|