dagster-dingtalk 0.1.8__tar.gz → 0.1.10b1__tar.gz
Sign up to get free protection for your applications and to get access to all the features.
- {dagster_dingtalk-0.1.8 → dagster_dingtalk-0.1.10b1}/PKG-INFO +8 -17
- dagster_dingtalk-0.1.10b1/README.md +21 -0
- {dagster_dingtalk-0.1.8 → dagster_dingtalk-0.1.10b1}/dagster_dingtalk/__init__.py +2 -2
- dagster_dingtalk-0.1.10b1/dagster_dingtalk/app_client.py +155 -0
- dagster_dingtalk-0.1.10b1/dagster_dingtalk/operations.py +20 -0
- dagster_dingtalk-0.1.10b1/dagster_dingtalk/resources.py +221 -0
- dagster_dingtalk-0.1.10b1/dagster_dingtalk/version.py +1 -0
- {dagster_dingtalk-0.1.8 → dagster_dingtalk-0.1.10b1}/pyproject.toml +1 -1
- dagster_dingtalk-0.1.8/README.md +0 -30
- dagster_dingtalk-0.1.8/dagster_dingtalk/operations.py +0 -21
- dagster_dingtalk-0.1.8/dagster_dingtalk/resources.py +0 -370
- dagster_dingtalk-0.1.8/dagster_dingtalk/version.py +0 -1
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: dagster-dingtalk
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.10b1
|
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,21 @@
|
|
1
|
+
# 钉钉与 Dagster 集成
|
2
|
+
|
3
|
+
---
|
4
|
+
|
5
|
+
## 介绍
|
6
|
+
|
7
|
+
该 Dagster 集成是为了更便捷的调用钉钉(DingTalk)的API,集成提供了两个 Dagster Resource。
|
8
|
+
|
9
|
+
|
10
|
+
## Webhook 资源
|
11
|
+
|
12
|
+
### DingTalkWebhookResource
|
13
|
+
|
14
|
+
该资源允许定义单个钉钉自定义机器人的 Webhook 端点,以便于发送文本、Markdown
|
15
|
+
、Link、 ActionCard、FeedCard 消息,消息具体样式可参考
|
16
|
+
[钉钉开放平台 | 自定义机器人发送消息的消息类型](https://open.dingtalk.com/document/orgapp/custom-bot-send-message-type) 。
|
17
|
+
|
18
|
+
|
19
|
+
### DingTalkAppResource
|
20
|
+
|
21
|
+
该 Dagster 资源允许定义一个钉钉的 API Client,更加便捷地调用钉钉服务端企业内部应用 API
|
@@ -1,8 +1,8 @@
|
|
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
6
|
# from dagster_dingtalk.operations import DingTalkWebhookOp
|
7
7
|
from dagster_dingtalk.version import __version__
|
8
8
|
|
@@ -0,0 +1,155 @@
|
|
1
|
+
import logging
|
2
|
+
import pickle
|
3
|
+
import time
|
4
|
+
from datetime import datetime
|
5
|
+
from enum import Enum
|
6
|
+
from pathlib import Path
|
7
|
+
from typing import List, Literal, Dict, Tuple
|
8
|
+
from httpx import Client
|
9
|
+
|
10
|
+
|
11
|
+
# noinspection NonAsciiCharacters
|
12
|
+
class DingTalkClient:
|
13
|
+
def __init__(self, app_id: str, client_id: str, client_secret: str, app_name: str|None = None, agent_id: int|None = None):
|
14
|
+
self.app_id: str = app_id
|
15
|
+
self.app_name: str|None = app_name
|
16
|
+
self.agent_id: int|None = agent_id
|
17
|
+
self.client_id: str = client_id
|
18
|
+
self.client_secret: str = client_secret
|
19
|
+
self.robot_code: str = client_id
|
20
|
+
|
21
|
+
access_token: str = self._get_access_token()
|
22
|
+
self.api: Client = Client(base_url="https://api.dingtalk.com/", headers={"x-acs-dingtalk-access-token": access_token})
|
23
|
+
self.oapi: Client = Client(base_url="https://oapi.dingtalk.com/", params={"access_token": access_token})
|
24
|
+
|
25
|
+
self.智能人事 = 智能人事_API(self)
|
26
|
+
self.通讯录管理 = 通讯录管理_API(self)
|
27
|
+
self.文档文件 = 文档文件_API(self)
|
28
|
+
|
29
|
+
def _get_access_token(self) -> str:
|
30
|
+
access_token_cache = Path("/tmp/.dingtalk_cache")
|
31
|
+
|
32
|
+
try:
|
33
|
+
with open(access_token_cache, 'rb') as f:
|
34
|
+
all_access_token: Dict[str, Tuple[str, int]] = pickle.loads(f.read())
|
35
|
+
access_token, expire_in = all_access_token.get(self.app_id, ('', 0))
|
36
|
+
except Exception as e:
|
37
|
+
print(e)
|
38
|
+
all_access_token = {}
|
39
|
+
access_token, expire_in = all_access_token.get(self.app_id, ('', 0))
|
40
|
+
|
41
|
+
if access_token and expire_in < int(time.time()):
|
42
|
+
return access_token
|
43
|
+
else:
|
44
|
+
logging.warning(f"应用{self.app_name}<{self.app_id}> 鉴权缓存过期或不存在,正在重新获取...")
|
45
|
+
response = Client().post(
|
46
|
+
url="https://api.dingtalk.com/v1.0/oauth2/accessToken",
|
47
|
+
json={"appKey": self.client_id, "appSecret": self.client_secret},
|
48
|
+
)
|
49
|
+
access_token:str = response.json().get("accessToken")
|
50
|
+
expire_in:int = response.json().get("expireIn") + int(time.time()) - 60
|
51
|
+
with open(access_token_cache, 'wb') as f:
|
52
|
+
all_access_token[self.app_id] = (access_token, expire_in)
|
53
|
+
f.write(pickle.dumps(all_access_token))
|
54
|
+
return access_token
|
55
|
+
|
56
|
+
|
57
|
+
# noinspection NonAsciiCharacters
|
58
|
+
class 智能人事_API:
|
59
|
+
def __init__(self, _client:DingTalkClient):
|
60
|
+
self.花名册 = 智能人事_花名册_API(_client)
|
61
|
+
self.员工管理 = 智能人事_员工管理_API(_client)
|
62
|
+
|
63
|
+
# noinspection NonAsciiCharacters
|
64
|
+
class 智能人事_花名册_API:
|
65
|
+
def __init__(self, _client:DingTalkClient):
|
66
|
+
self.__client:DingTalkClient = _client
|
67
|
+
|
68
|
+
def 获取花名册元数据(self) -> dict:
|
69
|
+
response = self.__client.oapi.post(
|
70
|
+
url="/topapi/smartwork/hrm/roster/meta/get",
|
71
|
+
json={"agentid": self.__client.agent_id},
|
72
|
+
)
|
73
|
+
return response.json()
|
74
|
+
|
75
|
+
def 获取员工花名册字段信息(self, user_id_list:List[str], field_filter_list:List[str]|None = None, text_to_select_convert:bool|None = None) -> dict:
|
76
|
+
body_dict = {"userIdList": user_id_list, "appAgentId": self.__client.agent_id}
|
77
|
+
if field_filter_list is not None:
|
78
|
+
body_dict["fieldFilterList"] = field_filter_list
|
79
|
+
if text_to_select_convert is not None:
|
80
|
+
body_dict["text2SelectConvert"] = text_to_select_convert
|
81
|
+
|
82
|
+
response = self.__client.api.post(url="/topapi/smartwork/hrm/roster/meta/get", json=body_dict, )
|
83
|
+
return response.json()
|
84
|
+
|
85
|
+
# noinspection NonAsciiCharacters
|
86
|
+
class 智能人事_员工管理_API:
|
87
|
+
|
88
|
+
def __init__(self, _client:DingTalkClient):
|
89
|
+
self.__client:DingTalkClient = _client
|
90
|
+
|
91
|
+
# noinspection NonAsciiCharacters
|
92
|
+
class 在职员工状态(Enum):
|
93
|
+
试用期: '2'
|
94
|
+
正式: '3'
|
95
|
+
待离职: '5'
|
96
|
+
无状态: '-1'
|
97
|
+
|
98
|
+
def 获取待入职员工列表(self, offset:int, size:int) -> dict:
|
99
|
+
response = self.__client.oapi.post(
|
100
|
+
"/topapi/smartwork/hrm/employee/querypreentry",
|
101
|
+
json={"offset": offset, "size": size},
|
102
|
+
)
|
103
|
+
return response.json()
|
104
|
+
|
105
|
+
def 获取在职员工列表(self, status_list:List[在职员工状态], offset:int, size:int) -> dict:
|
106
|
+
response = self.__client.oapi.post(
|
107
|
+
"/topapi/smartwork/hrm/employee/querypreentry",
|
108
|
+
json={"status_list": status_list, "offset": offset, "size": size},
|
109
|
+
)
|
110
|
+
return response.json()
|
111
|
+
|
112
|
+
def 获取离职员工列表(self, next_token:int, max_results:int) -> dict:
|
113
|
+
response = self.__client.api.get(
|
114
|
+
"/v1.0/hrm/employees/dismissions",
|
115
|
+
params={"nextToken": next_token, "maxResults": max_results},
|
116
|
+
)
|
117
|
+
return response.json()
|
118
|
+
|
119
|
+
def 批量获取员工离职信息(self, user_id_list:List[str]) -> dict:
|
120
|
+
response = self.__client.api.get(
|
121
|
+
"/v1.0/hrm/employees/dimissionInfo",
|
122
|
+
params={"userIdList": user_id_list},
|
123
|
+
)
|
124
|
+
return response.json()
|
125
|
+
|
126
|
+
# noinspection NonAsciiCharacters
|
127
|
+
class 通讯录管理_API:
|
128
|
+
def __init__(self, _client:DingTalkClient):
|
129
|
+
self.__client = _client
|
130
|
+
|
131
|
+
def 查询用户详情(self, user_id:str, language:str = "zh_CN") -> dict:
|
132
|
+
response = self.__client.oapi.post(url="/topapi/v2/user/get", json={"language": language, "userid": user_id})
|
133
|
+
return response.json()
|
134
|
+
|
135
|
+
def 查询离职记录列表(self, start_time:datetime, end_time:datetime|None, next_token:str, max_results:int) -> dict:
|
136
|
+
params = {"startTime": start_time.strftime("%Y-%m-%dT%H:%M:%SZ"), "nextToken": next_token, "maxResults": max_results}
|
137
|
+
if end_time is not None:
|
138
|
+
params["endTime"] = end_time.strftime("%Y-%m-%dT%H:%M:%SZ")
|
139
|
+
response = self.__client.api.get(url="/v1.0/contact/empLeaveRecords", params=params)
|
140
|
+
return response.json()
|
141
|
+
|
142
|
+
# noinspection NonAsciiCharacters
|
143
|
+
class 文档文件_API:
|
144
|
+
def __init__(self, _client:DingTalkClient):
|
145
|
+
self.媒体文件 = 文档文件_媒体文件_API(_client)
|
146
|
+
|
147
|
+
# noinspection NonAsciiCharacters
|
148
|
+
class 文档文件_媒体文件_API:
|
149
|
+
def __init__(self, _client:DingTalkClient):
|
150
|
+
self.__client = _client
|
151
|
+
|
152
|
+
def 上传媒体文件(self, file_path:Path|str, media_type:Literal['image', 'voice', 'video', 'file']) -> dict:
|
153
|
+
with open(file_path, 'rb') as f:
|
154
|
+
response = self.__client.oapi.post(url=f"/media/upload?type={media_type}", files={'media': f})
|
155
|
+
return response.json()
|
@@ -0,0 +1,20 @@
|
|
1
|
+
from dagster import In, OpExecutionContext, op
|
2
|
+
|
3
|
+
# noinspection PyProtectedMember
|
4
|
+
from dagster._annotations import experimental
|
5
|
+
|
6
|
+
|
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)
|
@@ -0,0 +1,221 @@
|
|
1
|
+
import base64
|
2
|
+
import hashlib
|
3
|
+
import hmac
|
4
|
+
import re
|
5
|
+
import time
|
6
|
+
import urllib.parse
|
7
|
+
from typing import Optional, Tuple, List, Literal
|
8
|
+
import httpx
|
9
|
+
from pydantic import Field
|
10
|
+
from app_client import DingTalkClient
|
11
|
+
from dagster import ConfigurableResource, InitResourceContext
|
12
|
+
|
13
|
+
|
14
|
+
class DingTalkWebhookResource(ConfigurableResource):
|
15
|
+
"""
|
16
|
+
定义一个钉钉群 Webhook 机器人资源,可以用来发送各类通知。
|
17
|
+
|
18
|
+
钉钉API文档:
|
19
|
+
https://open.dingtalk.com/document/orgapp/custom-bot-send-message-type
|
20
|
+
|
21
|
+
Args:
|
22
|
+
access_token (str): 机器人 Webhook 地址中的 access_token 值。
|
23
|
+
secret (str, optional): 如使用加签安全配置,则需传签名密钥。默认值为 None。
|
24
|
+
alias (str, optional): 如提供别名,可以在使用 `MultiDingTalkWebhookResource` 中使用别名进行 webhook 选择。默认值为 None。
|
25
|
+
base_url (str, optional): 通用地址,一般无需更改。默认值为 “https://oapi.dingtalk.com/robot/send”。
|
26
|
+
|
27
|
+
"""
|
28
|
+
access_token: str = Field(description="Webhook地址中的 access_token 部分")
|
29
|
+
secret: Optional[str] = Field(default=None, description="如使用加签安全配置,需传签名密钥")
|
30
|
+
alias: Optional[str] = Field(default=None, description="如提供别名,将来可以使用别名进行选择")
|
31
|
+
base_url: str = Field(default="https://oapi.dingtalk.com/robot/send", description="Webhook的通用地址,无需更改")
|
32
|
+
|
33
|
+
def webhook_url(self):
|
34
|
+
"""
|
35
|
+
实时生成加签或未加签的 Webhook URL。
|
36
|
+
|
37
|
+
钉钉API文档:
|
38
|
+
https://open.dingtalk.com/document/robots/custom-robot-access
|
39
|
+
|
40
|
+
Returns:
|
41
|
+
str: Webhook URL
|
42
|
+
"""
|
43
|
+
if self.secret is None:
|
44
|
+
return f"{self.base_url}?access_token={self.access_token}"
|
45
|
+
else:
|
46
|
+
timestamp = round(time.time() * 1000)
|
47
|
+
hmac_code = hmac.new(
|
48
|
+
self.secret.encode('utf-8'), f'{timestamp}\n{self.secret}'.encode('utf-8'), digestmod=hashlib.sha256
|
49
|
+
).digest()
|
50
|
+
sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))
|
51
|
+
return f"{self.base_url}?access_token={self.access_token}×tamp={timestamp}&sign={sign}"
|
52
|
+
|
53
|
+
@staticmethod
|
54
|
+
def _gen_title(text):
|
55
|
+
"""
|
56
|
+
从文本截取前12个字符作为标题,并清理其中的 Markdown 格式字符。
|
57
|
+
|
58
|
+
Args:
|
59
|
+
text: 原文
|
60
|
+
|
61
|
+
Returns:
|
62
|
+
str: 标题
|
63
|
+
"""
|
64
|
+
return re.sub(r'[\n#>* ]', '', text[:12])
|
65
|
+
|
66
|
+
def send_text(self, text: str,
|
67
|
+
at_mobiles:List[str]|None = None, at_user_ids:List[str]|None = None, at_all:bool = False):
|
68
|
+
"""
|
69
|
+
发送文本消息。
|
70
|
+
|
71
|
+
钉钉API文档:
|
72
|
+
https://open.dingtalk.com/document/orgapp/custom-bot-send-message-type
|
73
|
+
|
74
|
+
Args:
|
75
|
+
text (str): 待发送文本
|
76
|
+
at_mobiles (List[str], optional): 需要 @ 的用户手机号。默认值为 None
|
77
|
+
at_user_ids (List[str], optional): 需要 @ 的用户 UserID。默认值为 None
|
78
|
+
at_all (bool, optional): 是否 @ 所有人。默认值为 False
|
79
|
+
"""
|
80
|
+
at = {"isAtAll": at_all}
|
81
|
+
if at_user_ids:
|
82
|
+
at["atUserIds"] = at_user_ids
|
83
|
+
if at_mobiles:
|
84
|
+
at["atMobiles"] = at_mobiles
|
85
|
+
httpx.post(url=self.webhook_url(), json={"msgtype": "text", "text": {"content": text}, "at": at})
|
86
|
+
|
87
|
+
def send_link(self, text: str, message_url:str, title:str|None = None, pic_url:str = ""):
|
88
|
+
"""
|
89
|
+
发送 Link 消息。
|
90
|
+
|
91
|
+
钉钉API文档:
|
92
|
+
https://open.dingtalk.com/document/orgapp/custom-bot-send-message-type
|
93
|
+
|
94
|
+
Args:
|
95
|
+
text (str): 待发送文本
|
96
|
+
message_url (str): 链接的 Url
|
97
|
+
title (str, optional): 标题,在通知和被引用时显示的简短信息。默认从文本中生成。
|
98
|
+
pic_url (str, optional): 图片的 Url,默认为 None
|
99
|
+
"""
|
100
|
+
title = title or self._gen_title(text)
|
101
|
+
httpx.post(
|
102
|
+
url=self.webhook_url(),
|
103
|
+
json={"msgtype": "link", "link": {"title": title, "text": text, "picUrl": pic_url, "messageUrl": message_url}}
|
104
|
+
)
|
105
|
+
|
106
|
+
def send_markdown(self, text: List[str]|str, title:str|None = None,
|
107
|
+
at_mobiles:List[str]|None = None, at_user_ids:List[str]|None = None, at_all:bool = False):
|
108
|
+
"""
|
109
|
+
发送 Markdown 消息。支持的语法有:
|
110
|
+
# 一级标题
|
111
|
+
## 二级标题
|
112
|
+
### 三级标题
|
113
|
+
#### 四级标题
|
114
|
+
##### 五级标题
|
115
|
+
###### 六级标题
|
116
|
+
> 引用
|
117
|
+
**加粗**
|
118
|
+
*斜体*
|
119
|
+
[链接跳转](https://example.com/doc.html)
|
120
|
+
![图片预览](https://example.com/pic.jpg)
|
121
|
+
- 无序列表
|
122
|
+
1. 有序列表
|
123
|
+
|
124
|
+
钉钉API文档:
|
125
|
+
https://open.dingtalk.com/document/orgapp/custom-bot-send-message-type
|
126
|
+
|
127
|
+
Args:
|
128
|
+
text (str): 待发送文本
|
129
|
+
title (str, optional): 标题,在通知和被引用时显示的简短信息。默认从文本中生成。
|
130
|
+
at_mobiles (List[str], optional): 需要 @ 的用户手机号。默认值为 None
|
131
|
+
at_user_ids (List[str], optional): 需要 @ 的用户 UserID。默认值为 None
|
132
|
+
at_all (bool, optional): 是否 @ 所有人。默认值为 False
|
133
|
+
"""
|
134
|
+
text = text if isinstance(text, str) else "\n\n".join(text)
|
135
|
+
title = title or self._gen_title(text)
|
136
|
+
at = {"isAtAll": at_all}
|
137
|
+
if at_user_ids:
|
138
|
+
at["atUserIds"] = at_user_ids
|
139
|
+
if at_mobiles:
|
140
|
+
at["atMobiles"] = at_mobiles
|
141
|
+
httpx.post(url=self.webhook_url(),json={"msgtype": "markdown", "markdown": {"title": title, "text": text}, "at": at})
|
142
|
+
|
143
|
+
def send_action_card(self, text: List[str]|str, title:str|None = None, btn_orientation:Literal["0","1"] = "0",
|
144
|
+
single_jump:Tuple[str,str]|None = None, btns_jump:List[Tuple[str,str]]|None = None):
|
145
|
+
"""
|
146
|
+
发送跳转 ActionCard 消息。
|
147
|
+
|
148
|
+
Args:
|
149
|
+
text (str): 待发送文本,支持 Markdown 部分语法。
|
150
|
+
title (str, optional): 标题,在通知和被引用时显示的简短信息。默认从文本中生成。
|
151
|
+
btn_orientation (str, optional): 按钮排列方式,0-按钮竖直排列,1-按钮横向排列。默认值为 "0"
|
152
|
+
single_jump(Tuple[str,str], optional): 传此参数为单个按钮,元组内第一项为按钮的标题,第二项为按钮链接。
|
153
|
+
btns_jump(Tuple[str,str], optional): 传此参数为多个按钮,元组内第一项为按钮的标题,第二项为按钮链接。
|
154
|
+
|
155
|
+
Notes:
|
156
|
+
同时传 single_jump 和 btns_jump,仅 single_jump 生效。
|
157
|
+
|
158
|
+
"""
|
159
|
+
text = text if isinstance(text, str) else "\n\n".join(text)
|
160
|
+
title = title or self._gen_title(text)
|
161
|
+
action_card = {"title": title, "text": text, "btnOrientation": str(btn_orientation)}
|
162
|
+
if single_jump:
|
163
|
+
action_card["singleTitle"], action_card["singleURL"] = single_jump
|
164
|
+
httpx.post(url=self.webhook_url(), json={"msgtype": "actionCard", "actionCard": action_card})
|
165
|
+
elif btns_jump:
|
166
|
+
action_card["btns"] = [{"title": action_title, "actionURL": action_url} for action_title, action_url in btns_jump]
|
167
|
+
httpx.post(url=self.webhook_url(), json={"msgtype": "actionCard", "actionCard": action_card})
|
168
|
+
else:
|
169
|
+
pass
|
170
|
+
|
171
|
+
def send_feed_card(self, *args:Tuple[str,str,str]):
|
172
|
+
"""
|
173
|
+
发送 FeedCard 消息。
|
174
|
+
|
175
|
+
Args:
|
176
|
+
args (Tuple[str,str,str]): 可以传入任意个具有三个元素的元组,分别为 (标题, 跳转链接, 缩略图链接)
|
177
|
+
|
178
|
+
"""
|
179
|
+
for a in args:
|
180
|
+
print(a)
|
181
|
+
links_data = [
|
182
|
+
{"title": title, "messageURL": message_url, "picURL": pic_url}
|
183
|
+
for title, message_url, pic_url in args
|
184
|
+
]
|
185
|
+
httpx.post(url=self.webhook_url(), json={"msgtype": "feedCard", "feedCard": {"links": links_data}})
|
186
|
+
|
187
|
+
|
188
|
+
class DingTalkAppResource(ConfigurableResource):
|
189
|
+
"""
|
190
|
+
[钉钉服务端 API](https://open.dingtalk.com/document/orgapp/api-overview) 企业内部应用部分的第三方封装。
|
191
|
+
通过此资源,可以调用部分钉钉服务端API。
|
192
|
+
|
193
|
+
Notes:
|
194
|
+
不包含全部的API端点。
|
195
|
+
|
196
|
+
Args:
|
197
|
+
AppID (str): 应用应用唯一标识 AppID,作为缓存标识符使用。不传入则不缓存鉴权。
|
198
|
+
AgentID (int, optional): 原企业内部应用 AgentId ,部分 API 会使用到。默认值为 None
|
199
|
+
AppName (str, optional): 应用名。
|
200
|
+
ClientId (str): 应用的 Client ID ,原 AppKey 和 SuiteKey
|
201
|
+
ClientSecret (str): 应用的 Client Secret ,原 AppSecret 和 SuiteSecret
|
202
|
+
"""
|
203
|
+
|
204
|
+
AppID: str = Field(description="应用应用唯一标识 AppID,作为缓存标识符使用。不传入则不缓存鉴权。")
|
205
|
+
AgentID: Optional[int] = Field(default=None, description="原企业内部应用AgentId ,部分API会使用到。")
|
206
|
+
AppName: Optional[str] = Field(default=None, description="应用名。")
|
207
|
+
ClientId: str = Field(description="应用的 Client ID (原 AppKey 和 SuiteKey)")
|
208
|
+
ClientSecret: str = Field(description="应用的 Client Secret (原 AppSecret 和 SuiteSecret)")
|
209
|
+
|
210
|
+
@classmethod
|
211
|
+
def _is_dagster_maintained(cls) -> bool:
|
212
|
+
return False
|
213
|
+
|
214
|
+
def create_resource(self, context: InitResourceContext) -> DingTalkClient:
|
215
|
+
return DingTalkClient(
|
216
|
+
app_id=self.AppID,
|
217
|
+
agent_id=self.AgentID,
|
218
|
+
app_name=self.AppName,
|
219
|
+
client_id=self.ClientId,
|
220
|
+
client_secret=self.ClientSecret
|
221
|
+
)
|
@@ -0,0 +1 @@
|
|
1
|
+
__version__ = "0.1.10b1"
|
dagster_dingtalk-0.1.8/README.md
DELETED
@@ -1,30 +0,0 @@
|
|
1
|
-
# 钉钉与 Dagster 集成
|
2
|
-
|
3
|
-
---
|
4
|
-
|
5
|
-
## 介绍
|
6
|
-
|
7
|
-
该 Dagster 集成是为了更便捷的调用钉钉(DingTalk)的API,集成提供了两个 Dagster
|
8
|
-
Resource 和若干 Dagster Op 的封装。
|
9
|
-
|
10
|
-
### DingTalkWebhookResource
|
11
|
-
|
12
|
-
该 Dagster 资源允许定义一个钉钉自定义机器人的 Webhook 端点,发送文本、Markdown
|
13
|
-
、Link、 ActionCard、FeedCard 消息,消息具体样式可参考
|
14
|
-
[钉钉开放平台 | 自定义机器人发送消息的消息类型](https://open.dingtalk.com/document/orgapp/custom-bot-send-message-type) 。
|
15
|
-
|
16
|
-
|
17
|
-
### DingTalkAPIResource
|
18
|
-
|
19
|
-
该 Dagster 资源允许定义一个钉钉的 API Client,更加便捷地调用钉钉服务端 API (
|
20
|
-
仅企业内部应用)
|
21
|
-
|
22
|
-
|
23
|
-
### DingTalkWebhookOp
|
24
|
-
|
25
|
-
该类提供一些预定义的 Dagster Op ,以便于快速调用。
|
26
|
-
|
27
|
-
需要注意的是,这些 Op 都没有静态声明 `required_resource_keys`,而是需要根据
|
28
|
-
上下文配置 `context.op_config` 中的 `dingtalk_webhook_key` 键来动态查找
|
29
|
-
资源。在使用时,必须在 Job 中提供 `resource_defs` ,或在 `User Code` 全局
|
30
|
-
定义会使用到的 `dingtalk_webhook_key`。
|
@@ -1,21 +0,0 @@
|
|
1
|
-
from dagster import In, OpExecutionContext, op
|
2
|
-
|
3
|
-
|
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
|
-
|
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)
|
@@ -1,370 +0,0 @@
|
|
1
|
-
import base64
|
2
|
-
import hashlib
|
3
|
-
import hmac
|
4
|
-
import time
|
5
|
-
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
|
11
|
-
import httpx
|
12
|
-
|
13
|
-
from dagster import (
|
14
|
-
ConfigurableResource,
|
15
|
-
InitResourceContext, ResourceDependency,
|
16
|
-
)
|
17
|
-
from httpx import Client
|
18
|
-
from pydantic import Field, PrivateAttr
|
19
|
-
|
20
|
-
|
21
|
-
class DingTalkWebhookResource(ConfigurableResource):
|
22
|
-
access_token: str = Field(description="Webhook地址中的 access_token 部分")
|
23
|
-
secret: Optional[str] = Field(default=None, description="如使用加签安全配置,需传签名密钥")
|
24
|
-
alias: Optional[str] = Field(default=None, description="如提供别名,将来可以使用别名进行选择")
|
25
|
-
base_url: str = Field(default="https://oapi.dingtalk.com/robot/send", description="Webhook的通用地址,无需更改")
|
26
|
-
|
27
|
-
def webhook_url(self):
|
28
|
-
if self.secret is None:
|
29
|
-
return f"{self.base_url}?access_token={self.access_token}"
|
30
|
-
else:
|
31
|
-
timestamp = round(time.time() * 1000)
|
32
|
-
hmac_code = hmac.new(
|
33
|
-
self.secret.encode('utf-8'), f'{timestamp}\n{self.secret}'.encode('utf-8'), digestmod=hashlib.sha256
|
34
|
-
).digest()
|
35
|
-
sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))
|
36
|
-
return f"{self.base_url}?access_token={self.access_token}×tamp={timestamp}&sign={sign}"
|
37
|
-
|
38
|
-
@staticmethod
|
39
|
-
def _gen_title(text):
|
40
|
-
return text[:12].replace("\n", "").replace("#", "").replace(">", "").replace("*", "")
|
41
|
-
|
42
|
-
def send_text(self, text: str,
|
43
|
-
at_mobiles:List[str]|None = None, at_user_ids:List[str]|None = None, at_all:bool = False):
|
44
|
-
at = {"isAtAll": at_all}
|
45
|
-
if at_user_ids:
|
46
|
-
at["atUserIds"] = at_user_ids
|
47
|
-
if at_mobiles:
|
48
|
-
at["atMobiles"] = at_mobiles
|
49
|
-
httpx.post(url=self.webhook_url(), json={"msgtype": "text", "text": {"content": text}, "at": at})
|
50
|
-
|
51
|
-
def send_link(self, text: str, message_url:str, title:str|None = None, pic_url:str = ""):
|
52
|
-
title = title or self._gen_title(text)
|
53
|
-
httpx.post(
|
54
|
-
url=self.webhook_url(),
|
55
|
-
json={"msgtype": "link", "link": {"title": title, "text": text, "picUrl": pic_url, "messageUrl": message_url}}
|
56
|
-
)
|
57
|
-
|
58
|
-
def send_markdown(self, text: List[str]|str, title:str|None = None,
|
59
|
-
at_mobiles:List[str]|None = None, at_user_ids:List[str]|None = None, at_all:bool = False):
|
60
|
-
text = text if isinstance(text, str) else "\n\n".join(text)
|
61
|
-
title = title or self._gen_title(text)
|
62
|
-
at = {"isAtAll": at_all}
|
63
|
-
if at_user_ids:
|
64
|
-
at["atUserIds"] = at_user_ids
|
65
|
-
if at_mobiles:
|
66
|
-
at["atMobiles"] = at_mobiles
|
67
|
-
httpx.post(url=self.webhook_url(),json={"msgtype": "markdown", "markdown": {"title": title, "text": text}, "at": at})
|
68
|
-
|
69
|
-
def send_action_card(self, text: List[str]|str, title:str|None = None, btn_orientation:Literal["0","1"] = "0",
|
70
|
-
single_jump:Tuple[str,str]|None = None, btns_jump:List[Tuple[str,str]]|None = None):
|
71
|
-
text = text if isinstance(text, str) else "\n\n".join(text)
|
72
|
-
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:
|
78
|
-
action_card["singleTitle"], action_card["singleURL"] = single_jump
|
79
|
-
httpx.post(url=self.webhook_url(), json={"msgtype": "actionCard", "actionCard": action_card})
|
80
|
-
else:
|
81
|
-
pass
|
82
|
-
|
83
|
-
def send_feed_card(self, links: List[Tuple[str,str,str]]):
|
84
|
-
links_data = [
|
85
|
-
{"title": title, "messageURL": message_url, "picURL": pic_url}
|
86
|
-
for title, message_url, pic_url in links
|
87
|
-
]
|
88
|
-
httpx.post(url=self.webhook_url(), json={"msgtype": "feedCard", "feedCard": {"links": links_data}})
|
89
|
-
|
90
|
-
|
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):
|
159
|
-
"""
|
160
|
-
[钉钉服务端 API](https://open.dingtalk.com/document/orgapp/api-overview) 企业内部应用部分的第三方封装。
|
161
|
-
通过此资源,可以调用部分钉钉服务端API。
|
162
|
-
|
163
|
-
注意:不包含全部的API端点。
|
164
|
-
"""
|
165
|
-
|
166
|
-
AppID: str = Field(description="应用应用唯一标识 AppID,作为缓存标识符使用。不传入则不缓存鉴权。")
|
167
|
-
AgentID: Optional[int] = Field(default=None, description="原企业内部应用AgentId ,部分API会使用到。")
|
168
|
-
AppName: Optional[str] = Field(default=None, description="应用名。")
|
169
|
-
ClientId: str = Field(description="应用的 Client ID (原 AppKey 和 SuiteKey)")
|
170
|
-
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
|
-
|
175
|
-
@classmethod
|
176
|
-
def _is_dagster_maintained(cls) -> bool:
|
177
|
-
return False
|
178
|
-
|
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},
|
340
|
-
)
|
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 +0,0 @@
|
|
1
|
-
__version__ = "0.0.1"
|