pytbox 0.0.1__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.
Potentially problematic release.
This version of pytbox might be problematic. Click here for more details.
- pytbox/alicloud/sls.py +93 -0
- pytbox/common/__init__.py +7 -0
- pytbox/common/base.py +0 -0
- pytbox/dida365.py +297 -0
- pytbox/feishu/client.py +183 -0
- pytbox/feishu/endpoints.py +1049 -0
- pytbox/feishu/errors.py +45 -0
- pytbox/feishu/helpers.py +7 -0
- pytbox/feishu/typing.py +7 -0
- pytbox/logger.py +126 -0
- pytbox/onepassword_connect.py +92 -0
- pytbox/onepassword_sa.py +197 -0
- pytbox/utils/env.py +30 -0
- pytbox/utils/response.py +45 -0
- pytbox/utils/timeutils.py +40 -0
- pytbox/victorialog.py +125 -0
- pytbox/victoriametrics.py +37 -0
- pytbox-0.0.1.dist-info/METADATA +264 -0
- pytbox-0.0.1.dist-info/RECORD +21 -0
- pytbox-0.0.1.dist-info/WHEEL +5 -0
- pytbox-0.0.1.dist-info/top_level.txt +1 -0
pytbox/alicloud/sls.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
# 引入sls包。
|
|
5
|
+
from aliyun.log import GetLogsRequest, LogItem, PutLogsRequest
|
|
6
|
+
from aliyun.log import LogClient as SlsLogClient
|
|
7
|
+
from aliyun.log.auth import AUTH_VERSION_4
|
|
8
|
+
from ..utils.env import check_env
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AliCloudSls:
|
|
13
|
+
|
|
14
|
+
def __init__(self, access_key_id: str=None, access_key_secret: str=None, project: str, logstore: str):
|
|
15
|
+
# 日志服务的服务接入点
|
|
16
|
+
self.endpoint = "cn-shanghai.log.aliyuncs.com"
|
|
17
|
+
# 创建 LogClient 实例,使用 V4 签名,根据实际情况填写 region,这里以杭州为例
|
|
18
|
+
self.client = SlsLogClient(self.endpoint, access_key_id, access_key_secret, auth_version=AUTH_VERSION_4, region='cn-shanghai')
|
|
19
|
+
self.project = project
|
|
20
|
+
self.logstore = logstore
|
|
21
|
+
|
|
22
|
+
def get_logs(self, project_name, logstore_name, query, from_time, to_time):
|
|
23
|
+
# Project名称。
|
|
24
|
+
# project_name = "sh-prod-network-devices-log"
|
|
25
|
+
# Logstore名称
|
|
26
|
+
# logstore_name = "sh-prod-network-devices-log"
|
|
27
|
+
# 查询语句。
|
|
28
|
+
# query = "*| select dev,id from " + logstore_name
|
|
29
|
+
# query = "*"
|
|
30
|
+
# 索引。
|
|
31
|
+
logstore_index = {'line': {
|
|
32
|
+
'token': [',', ' ', "'", '"', ';', '=', '(', ')', '[', ']', '{', '}', '?', '@', '&', '<', '>', '/', ':', '\n', '\t',
|
|
33
|
+
'\r'], 'caseSensitive': False, 'chn': False}, 'keys': {'dev': {'type': 'text',
|
|
34
|
+
'token': [',', ' ', "'", '"', ';', '=',
|
|
35
|
+
'(', ')', '[', ']', '{', '}',
|
|
36
|
+
'?', '@', '&', '<', '>', '/',
|
|
37
|
+
':', '\n', '\t', '\r'],
|
|
38
|
+
'caseSensitive': False, 'alias': '',
|
|
39
|
+
'doc_value': True, 'chn': False},
|
|
40
|
+
'id': {'type': 'long', 'alias': '',
|
|
41
|
+
'doc_value': True}}, 'log_reduce': False,
|
|
42
|
+
'max_text_len': 2048}
|
|
43
|
+
|
|
44
|
+
# from_time和to_time表示查询日志的时间范围,Unix时间戳格式。
|
|
45
|
+
# from_time = int(time.time()) - 60
|
|
46
|
+
# to_time = time.time() + 60
|
|
47
|
+
# # 通过SQL查询日志。
|
|
48
|
+
# def get_logs():
|
|
49
|
+
# print("ready to query logs from logstore %s" % logstore_name)
|
|
50
|
+
request = GetLogsRequest(project_name, logstore_name, from_time, to_time, query=query)
|
|
51
|
+
response = self.client.get_logs(request)
|
|
52
|
+
for log in response.get_logs():
|
|
53
|
+
yield log.contents
|
|
54
|
+
|
|
55
|
+
def put_logs(self,
|
|
56
|
+
topic: Literal['meraki_alert', 'program']='program',
|
|
57
|
+
level: Literal['INFO', 'WARN']='INFO',
|
|
58
|
+
msg: str=None,
|
|
59
|
+
app: str=None,
|
|
60
|
+
caller_filename: str=None,
|
|
61
|
+
caller_lineno: int=None,
|
|
62
|
+
caller_function: str=None,
|
|
63
|
+
call_full_filename: str=None
|
|
64
|
+
):
|
|
65
|
+
log_group = []
|
|
66
|
+
log_item = LogItem()
|
|
67
|
+
contents = [
|
|
68
|
+
('env', check_env()),
|
|
69
|
+
('level', level),
|
|
70
|
+
('app', app),
|
|
71
|
+
('msg', msg),
|
|
72
|
+
('caller_filename', caller_filename),
|
|
73
|
+
('caller_lineno', str(caller_lineno)),
|
|
74
|
+
('caller_function', caller_function),
|
|
75
|
+
('call_full_filename', call_full_filename)
|
|
76
|
+
]
|
|
77
|
+
log_item.set_contents(contents)
|
|
78
|
+
log_group.append(log_item)
|
|
79
|
+
request = PutLogsRequest(self.project, self.logstore, topic, "", log_group, compress=False)
|
|
80
|
+
self.client.put_logs(request)
|
|
81
|
+
|
|
82
|
+
def put_logs_for_meraki(self, alert):
|
|
83
|
+
log_group = []
|
|
84
|
+
log_item = LogItem()
|
|
85
|
+
contents = alert
|
|
86
|
+
log_item.set_contents(contents)
|
|
87
|
+
log_group.append(log_item)
|
|
88
|
+
request = PutLogsRequest(self.project, self.logstore, "", "", log_group, compress=False)
|
|
89
|
+
self.client.put_logs(request)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
if __name__ == "__main__":
|
|
93
|
+
pass
|
pytbox/common/base.py
ADDED
|
File without changes
|
pytbox/dida365.py
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""滴答清单API客户端。
|
|
4
|
+
|
|
5
|
+
此模块提供了与滴答清单API交互的功能,包括认证和基本操作。
|
|
6
|
+
"""
|
|
7
|
+
import re
|
|
8
|
+
import json
|
|
9
|
+
from typing import Dict, Any, Literal, Union
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
import requests
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from .utils.response import ReturnResponse
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class Task:
|
|
18
|
+
task_id: str
|
|
19
|
+
project_id: str
|
|
20
|
+
title: str
|
|
21
|
+
content: str
|
|
22
|
+
desc: str
|
|
23
|
+
start_date: str
|
|
24
|
+
due_date: str
|
|
25
|
+
priority: int
|
|
26
|
+
status: int
|
|
27
|
+
tags: list
|
|
28
|
+
completed_time: str
|
|
29
|
+
assignee: int
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class DidaResponse:
|
|
33
|
+
code: int
|
|
34
|
+
message: str
|
|
35
|
+
data: dict
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ProcessDidaResponse:
|
|
39
|
+
@staticmethod
|
|
40
|
+
def status(status):
|
|
41
|
+
if status == 0:
|
|
42
|
+
return '进行中'
|
|
43
|
+
elif status == 2:
|
|
44
|
+
return '已完成'
|
|
45
|
+
else:
|
|
46
|
+
return '未识别'
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def priority(priority):
|
|
50
|
+
if priority == 1:
|
|
51
|
+
return '低优先级'
|
|
52
|
+
elif priority == 3:
|
|
53
|
+
return '中优先级'
|
|
54
|
+
elif priority == 5:
|
|
55
|
+
return '高优先级'
|
|
56
|
+
else:
|
|
57
|
+
return '未识别'
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Dida365:
|
|
61
|
+
"""滴答清单API客户端类。
|
|
62
|
+
|
|
63
|
+
处理滴答清单API的认证和操作。
|
|
64
|
+
|
|
65
|
+
Attributes:
|
|
66
|
+
config: 滴答清单配置实例
|
|
67
|
+
access_token: 访问令牌
|
|
68
|
+
refresh_token: 刷新令牌
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(self, access_token: str, cookie: str) -> None:
|
|
72
|
+
"""初始化客户端。
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
access_token: 滴答清单配置实例
|
|
76
|
+
"""
|
|
77
|
+
self.access_token = access_token
|
|
78
|
+
self.base_url = 'https://api.dida365.com'
|
|
79
|
+
self.cookie = cookie
|
|
80
|
+
self.headers = {
|
|
81
|
+
"Authorization": f"Bearer {self.access_token}",
|
|
82
|
+
"Content-Type": "application/json",
|
|
83
|
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
|
|
84
|
+
}
|
|
85
|
+
self.cookie_headers = {
|
|
86
|
+
"Content-Type": "application/json",
|
|
87
|
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
|
88
|
+
"Cookie": self.cookie
|
|
89
|
+
}
|
|
90
|
+
self.timeout = 10
|
|
91
|
+
|
|
92
|
+
def request(self, api_url: str=None, method: str='GET', payload: dict={}):
|
|
93
|
+
"""发送请求。
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
url: 请求URL
|
|
97
|
+
method: 请求方法
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
url = f"{self.base_url}{api_url}"
|
|
101
|
+
response = requests.request(
|
|
102
|
+
method=method,
|
|
103
|
+
url=url,
|
|
104
|
+
headers=self.headers,
|
|
105
|
+
data=json.dumps(payload),
|
|
106
|
+
timeout=3
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
if response.status_code == 200:
|
|
110
|
+
if 'complete' in api_url:
|
|
111
|
+
return DidaResponse(code=0, msg='success', data=None)
|
|
112
|
+
else:
|
|
113
|
+
try:
|
|
114
|
+
return DidaResponse(code=0, msg='success', data=response.json())
|
|
115
|
+
except Exception as e:
|
|
116
|
+
return DidaResponse(code=1, msg='warning', data=response)
|
|
117
|
+
else:
|
|
118
|
+
return DidaResponse(code=1, msg='error', data=response.json())
|
|
119
|
+
|
|
120
|
+
def task_list(self, project_id: str, enhancement: bool=True):
|
|
121
|
+
"""获取任务列表。
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
List[Dict[str, Any]]: 任务列表
|
|
125
|
+
|
|
126
|
+
Raises:
|
|
127
|
+
requests.exceptions.RequestException: 请求失败时抛出
|
|
128
|
+
ValueError: 没有访问令牌时抛出
|
|
129
|
+
"""
|
|
130
|
+
if enhancement:
|
|
131
|
+
tasks = requests.request(
|
|
132
|
+
method='GET',
|
|
133
|
+
url=f'https://api.dida365.com/api/v2/project/{project_id}/tasks',
|
|
134
|
+
headers=self.cookie_headers,
|
|
135
|
+
timeout=3
|
|
136
|
+
).json()
|
|
137
|
+
for task in tasks:
|
|
138
|
+
yield Task(task_id=task.get('id'),
|
|
139
|
+
project_id=task.get('projectId'),
|
|
140
|
+
title=task.get('title'),
|
|
141
|
+
content=task.get('content'),
|
|
142
|
+
desc=task.get('desc'),
|
|
143
|
+
start_date=task.get('startDate'),
|
|
144
|
+
due_date=task.get('dueDate'),
|
|
145
|
+
priority=ProcessDidaResponse.priority(task.get('priority')),
|
|
146
|
+
status=ProcessDidaResponse.status(task.get('status')),
|
|
147
|
+
tags=task.get('tags'),
|
|
148
|
+
completed_time=task.get('completedTime'),
|
|
149
|
+
assignee=task.get('assignee'))
|
|
150
|
+
else:
|
|
151
|
+
tasks = self.request(api_url=f"/open/v1/project/{project_id}/data", method="GET")['tasks']
|
|
152
|
+
for task in tasks:
|
|
153
|
+
yield Task(task_id=task.get('id'),
|
|
154
|
+
project_id=task.get('projectId'),
|
|
155
|
+
title=task.get('title'),
|
|
156
|
+
content=task.get('content'),
|
|
157
|
+
desc=task.get('desc'),
|
|
158
|
+
start_date=task.get('startDate'),
|
|
159
|
+
due_date=task.get('dueDate'),
|
|
160
|
+
priority=ProcessDidaResponse.priority(task.get('priority')),
|
|
161
|
+
status=ProcessDidaResponse.status(task.get('status')),
|
|
162
|
+
tags=task.get('tags'),
|
|
163
|
+
completed_time=task.get('completedTime'),
|
|
164
|
+
assignee=task.get('assignee'))
|
|
165
|
+
|
|
166
|
+
def task_create(self,
|
|
167
|
+
project_id,
|
|
168
|
+
title :str,
|
|
169
|
+
content :str=None,
|
|
170
|
+
tags :list=None,
|
|
171
|
+
priority :Literal[1, 3, 5]=1,
|
|
172
|
+
start_date: datetime=datetime.utcnow(),
|
|
173
|
+
start_time_offset: bool=True,
|
|
174
|
+
due_date: str=None,
|
|
175
|
+
kind: Union[None, Literal["NOTE"], str] = 'TEXT',
|
|
176
|
+
assignee: int=None,
|
|
177
|
+
reminder: bool=True):
|
|
178
|
+
'''
|
|
179
|
+
_summary_
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
project_id (_type_): _description_
|
|
183
|
+
title (str): _description_
|
|
184
|
+
content (str, optional): _description_. Defaults to None.
|
|
185
|
+
tags (list, optional): _description_. Defaults to None.
|
|
186
|
+
priority (Literal[1, 3, 5], optional): _description_. Defaults to 1.
|
|
187
|
+
start_date (datetime, optional): 传入 utc 时区的时间对象. Defaults to datetime.utcnow().
|
|
188
|
+
due_date (str, optional): _description_. Defaults to None.
|
|
189
|
+
kind (Union[None, Literal["NOTE"], str], optional): _description_. Defaults to 'TEXT'.
|
|
190
|
+
assignee (int, optional): _description_. Defaults to None.
|
|
191
|
+
reminder (bool, optional): _description_. Defaults to True.
|
|
192
|
+
'''
|
|
193
|
+
# 如果存在start_date,将其增加3分钟
|
|
194
|
+
if isinstance(start_date, datetime):
|
|
195
|
+
if start_time_offset:
|
|
196
|
+
if start_date.minute + 3 >= 60:
|
|
197
|
+
minute = 59
|
|
198
|
+
else:
|
|
199
|
+
minute = start_date.minute + 3
|
|
200
|
+
start_date_offset = start_date.replace(minute=minute)
|
|
201
|
+
start_date_format = start_date_offset.strftime('%Y-%m-%dT%H:%M:%S.000+0000')
|
|
202
|
+
else:
|
|
203
|
+
start_date_format = start_date.strftime('%Y-%m-%dT%H:%M:%S.000+0000')
|
|
204
|
+
else:
|
|
205
|
+
start_date_format = start_date.strftime('%Y-%m-%dT%H:%M:%S.000+0000')
|
|
206
|
+
|
|
207
|
+
payload = {
|
|
208
|
+
"projectId": project_id,
|
|
209
|
+
"priority": priority,
|
|
210
|
+
"assignee": str(assignee),
|
|
211
|
+
# "startDate": start_date_format,
|
|
212
|
+
"title": title,
|
|
213
|
+
"timeZone": "Asia/Shanghai",
|
|
214
|
+
"kind": kind,
|
|
215
|
+
"content": content,
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
payload['startDate'] = start_date_format
|
|
219
|
+
|
|
220
|
+
if isinstance(due_date, datetime):
|
|
221
|
+
due_date_format = due_date.strftime('%Y-%m-%dT%H:%M:%S.000+0000')
|
|
222
|
+
payload['dueDate'] = due_date_format
|
|
223
|
+
|
|
224
|
+
if reminder:
|
|
225
|
+
payload["reminders"] = [
|
|
226
|
+
"TRIGGER:PT0S"
|
|
227
|
+
]
|
|
228
|
+
if tags:
|
|
229
|
+
payload['tags'] = tags
|
|
230
|
+
|
|
231
|
+
return self.request(api_url="/open/v1/task", method="POST", payload=payload)
|
|
232
|
+
|
|
233
|
+
def task_complete(self, project_id: str, task_id: str):
|
|
234
|
+
"""完成任务。
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
project_id: 项目ID
|
|
238
|
+
task_id: 任务ID
|
|
239
|
+
"""
|
|
240
|
+
return self.request(api_url=f"/open/v1/project/{project_id}/task/{task_id}/complete", method="POST")
|
|
241
|
+
|
|
242
|
+
def task_get(self, project_id, task_id):
|
|
243
|
+
return self.request(api_url = f'/open/v1/project/{project_id}/task/{task_id}')
|
|
244
|
+
|
|
245
|
+
def task_comments(self, project_id: str, task_id: str):
|
|
246
|
+
return requests.request(
|
|
247
|
+
method='GET',
|
|
248
|
+
url=f'https://api.dida365.com/api/v2/project/{project_id}/task/{task_id}/comments',
|
|
249
|
+
headers=self.cookie_headers,
|
|
250
|
+
timeout=3
|
|
251
|
+
).json()
|
|
252
|
+
|
|
253
|
+
def task_update(self, project_id: str=None, task_id: str=None, title: str=None, content: str=None, priority: int=None, start_date: str=None, content_front: bool=False):
|
|
254
|
+
"""更新任务。
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
project_id: 项目ID
|
|
258
|
+
task_id: 任务ID
|
|
259
|
+
"""
|
|
260
|
+
task_get_resp = self.task_get(project_id, task_id)
|
|
261
|
+
if task_get_resp.code == 0:
|
|
262
|
+
exists_content = task_get_resp.data['content']
|
|
263
|
+
|
|
264
|
+
if content_front:
|
|
265
|
+
content = f'{content}\n{exists_content}'
|
|
266
|
+
else:
|
|
267
|
+
content = f'{exists_content}\n{content}'
|
|
268
|
+
|
|
269
|
+
elif task_get_resp.code == 1:
|
|
270
|
+
return task_get_resp
|
|
271
|
+
|
|
272
|
+
payload = {
|
|
273
|
+
"projectId": project_id,
|
|
274
|
+
"taskId": task_id,
|
|
275
|
+
"title": title,
|
|
276
|
+
"content": content,
|
|
277
|
+
"priority": priority,
|
|
278
|
+
}
|
|
279
|
+
if start_date:
|
|
280
|
+
payload["startDate"] = start_date
|
|
281
|
+
|
|
282
|
+
return self.request(api_url=f"/open/v1/task/{task_id}", method="POST", payload=payload)
|
|
283
|
+
|
|
284
|
+
def get_projects(self) -> ReturnResponse:
|
|
285
|
+
response = requests.request(
|
|
286
|
+
method='GET',
|
|
287
|
+
url=f'{self.base_url}/open/v1/project',
|
|
288
|
+
headers=self.headers,
|
|
289
|
+
timeout=self.timeout
|
|
290
|
+
)
|
|
291
|
+
if response.status_code == 200:
|
|
292
|
+
return ReturnResponse(code=0, msg=f"获取到 {len(response.json())} 条 project", data=response.json())
|
|
293
|
+
else:
|
|
294
|
+
return ReturnResponse(code=1, msg=f"获取 project 失败: {response.status_code}", data=response.json())
|
|
295
|
+
|
|
296
|
+
if __name__ == "__main__":
|
|
297
|
+
pass
|
pytbox/feishu/client.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
from typing import Optional, Union, Any, Dict, Type, List
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from .endpoints import (
|
|
6
|
+
AuthEndpoint,
|
|
7
|
+
MessageEndpoint,
|
|
8
|
+
ExtensionsEndpoint,
|
|
9
|
+
BitableEndpoint,
|
|
10
|
+
DocsEndpoint,
|
|
11
|
+
CalendarEndpoint
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from .errors import (
|
|
15
|
+
RequestTimeoutError,
|
|
16
|
+
is_api_error_code,
|
|
17
|
+
APIResponseError,
|
|
18
|
+
HTTPResponseError
|
|
19
|
+
)
|
|
20
|
+
from types import TracebackType
|
|
21
|
+
from abc import abstractclassmethod
|
|
22
|
+
import httpx
|
|
23
|
+
from httpx import Request, Response
|
|
24
|
+
from .typing import SyncAsync
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class ClientOptions:
|
|
28
|
+
auth: Optional[str] = None
|
|
29
|
+
timeout_ms: int = 60_000
|
|
30
|
+
base_url: str = "https://open.feishu.cn/open-apis"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class FeishuResponse:
|
|
35
|
+
code: int
|
|
36
|
+
data: dict
|
|
37
|
+
chat_id: str
|
|
38
|
+
message_id: str
|
|
39
|
+
msg_type: str
|
|
40
|
+
sender: dict
|
|
41
|
+
msg: dict
|
|
42
|
+
expire: int
|
|
43
|
+
tenant_access_token: str
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class BaseClient:
|
|
47
|
+
|
|
48
|
+
def __init__(self,
|
|
49
|
+
app_id: str,
|
|
50
|
+
app_secret: str,
|
|
51
|
+
client: Union[httpx.Client, httpx.AsyncClient],
|
|
52
|
+
) -> None:
|
|
53
|
+
|
|
54
|
+
self.app_id = app_id
|
|
55
|
+
self.app_secret = app_secret
|
|
56
|
+
|
|
57
|
+
self.options = ClientOptions()
|
|
58
|
+
|
|
59
|
+
self._clients: List[Union[httpx.Client, httpx.AsyncClient]] = []
|
|
60
|
+
self.client = client
|
|
61
|
+
|
|
62
|
+
self.auth = AuthEndpoint(self)
|
|
63
|
+
self.message = MessageEndpoint(self)
|
|
64
|
+
self.bitable = BitableEndpoint(self)
|
|
65
|
+
self.docs = DocsEndpoint(self)
|
|
66
|
+
self.calendar = CalendarEndpoint(self)
|
|
67
|
+
self.extensions = ExtensionsEndpoint(self)
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def client(self) -> Union[httpx.Client, httpx.AsyncClient]:
|
|
71
|
+
return self._clients[-1]
|
|
72
|
+
|
|
73
|
+
@client.setter
|
|
74
|
+
def client(self, client: Union[httpx.Client, httpx.AsyncClient]) -> None:
|
|
75
|
+
client.base_url = httpx.URL(f'{self.options.base_url}/')
|
|
76
|
+
client.timeout = httpx.Timeout(timeout=self.options.timeout_ms / 1_000)
|
|
77
|
+
client.headers = httpx.Headers(
|
|
78
|
+
{
|
|
79
|
+
"User-Agent": "cc_feishu",
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
self._clients.append(client)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _build_request(self,
|
|
86
|
+
method: str,
|
|
87
|
+
path: str,
|
|
88
|
+
query: Optional[Dict[str, Any]] = None,
|
|
89
|
+
body: Optional[Dict[str, Any]] = None,
|
|
90
|
+
data: Optional[Any] = None,
|
|
91
|
+
files: Optional[Dict[str, Any]] = None,
|
|
92
|
+
token: Optional[str] = None) -> Request:
|
|
93
|
+
|
|
94
|
+
headers = httpx.Headers()
|
|
95
|
+
headers['Authorization'] = f'Bearer {token}'
|
|
96
|
+
if 'image' in path:
|
|
97
|
+
headers['Content-Type'] = 'multipart/form-data'
|
|
98
|
+
|
|
99
|
+
return self.client.build_request(
|
|
100
|
+
method=method, url=path, params=query, json=body, headers=headers, files=files, data=data
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def _parse_response(self, response) -> Any:
|
|
104
|
+
response = response.json()
|
|
105
|
+
return FeishuResponse(code=response.get('code'),
|
|
106
|
+
data=response.get('data'),
|
|
107
|
+
chat_id=response.get('chat_id'),
|
|
108
|
+
message_id=response.get('message_id'),
|
|
109
|
+
msg_type=response.get('msg_type'),
|
|
110
|
+
sender=response.get('sender'),
|
|
111
|
+
msg=response.get('msg'),
|
|
112
|
+
expire=response.get('expire'),
|
|
113
|
+
tenant_access_token=response.get('tenant_access_token'))
|
|
114
|
+
|
|
115
|
+
@abstractclassmethod
|
|
116
|
+
def request(self,
|
|
117
|
+
path: str,
|
|
118
|
+
method: str,
|
|
119
|
+
query: Optional[Dict[Any, Any]] = None,
|
|
120
|
+
body: Optional[Dict[Any, Any]] = None,
|
|
121
|
+
auth: Optional[str] = None,
|
|
122
|
+
data: Optional[Any] = None,
|
|
123
|
+
) -> SyncAsync[Any]:
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class Client(BaseClient):
|
|
128
|
+
|
|
129
|
+
client: httpx.Client
|
|
130
|
+
|
|
131
|
+
def __init__(self,
|
|
132
|
+
app_id: str,
|
|
133
|
+
app_secret: str,
|
|
134
|
+
client: Optional[httpx.Client]=None) -> None:
|
|
135
|
+
|
|
136
|
+
if client is None:
|
|
137
|
+
client = httpx.Client()
|
|
138
|
+
super().__init__(app_id, app_secret, client)
|
|
139
|
+
|
|
140
|
+
def __enter__(self) -> "Client":
|
|
141
|
+
self.client = httpx.Client()
|
|
142
|
+
self.client.__enter__()
|
|
143
|
+
return self
|
|
144
|
+
|
|
145
|
+
def __exit__(self,
|
|
146
|
+
exc_type: Type[BaseException],
|
|
147
|
+
exc_value: BaseException,
|
|
148
|
+
traceback: TracebackType) -> None:
|
|
149
|
+
self.client.__exit__(exc_type, exc_value, traceback)
|
|
150
|
+
del self._clients[-1]
|
|
151
|
+
|
|
152
|
+
def close(self) -> None:
|
|
153
|
+
self.client.close()
|
|
154
|
+
|
|
155
|
+
def _get_token(self):
|
|
156
|
+
if self.auth.fetch_token_from_file():
|
|
157
|
+
return self.auth.fetch_token_from_file()
|
|
158
|
+
else:
|
|
159
|
+
self.auth.save_token_to_file()
|
|
160
|
+
return self.auth.fetch_token_from_file()
|
|
161
|
+
|
|
162
|
+
def request(self,
|
|
163
|
+
path: str,
|
|
164
|
+
method: str,
|
|
165
|
+
query: Optional[Dict[Any, Any]] = None,
|
|
166
|
+
body: Optional[Dict[Any, Any]] = None,
|
|
167
|
+
files: Optional[Dict[Any, Any]] = None,
|
|
168
|
+
token: Optional[str] = None,
|
|
169
|
+
data: Optional[Any] = None,
|
|
170
|
+
) -> Any:
|
|
171
|
+
|
|
172
|
+
request = self._build_request(method, path, query, body, files=files, data=data, token=self._get_token())
|
|
173
|
+
try:
|
|
174
|
+
response = self._parse_response(self.client.send(request))
|
|
175
|
+
|
|
176
|
+
if 'Invalid access token for authorization' in response.msg:
|
|
177
|
+
self.auth.save_token_to_file()
|
|
178
|
+
request = self._build_request(method, path, query, body, files=files, data=data, token=self._get_token())
|
|
179
|
+
return self._parse_response(self.client.send(request))
|
|
180
|
+
else:
|
|
181
|
+
return response
|
|
182
|
+
except httpx.TimeoutException:
|
|
183
|
+
raise RequestTimeoutError()
|