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

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,,