pytbox 0.1.2__py3-none-any.whl → 0.1.3__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/base.py +11 -1
- pytbox/database/victoriametrics.py +10 -0
- pytbox/excel.py +0 -0
- pytbox/feishu/endpoints.py +6 -6
- pytbox/mail/client.py +84 -138
- pytbox/mail/mail_detail.py +30 -0
- pytbox/pyjira.py +558 -0
- {pytbox-0.1.2.dist-info → pytbox-0.1.3.dist-info}/METADATA +3 -1
- {pytbox-0.1.2.dist-info → pytbox-0.1.3.dist-info}/RECORD +12 -9
- {pytbox-0.1.2.dist-info → pytbox-0.1.3.dist-info}/WHEEL +0 -0
- {pytbox-0.1.2.dist-info → pytbox-0.1.3.dist-info}/entry_points.txt +0 -0
- {pytbox-0.1.2.dist-info → pytbox-0.1.3.dist-info}/top_level.txt +0 -0
pytbox/base.py
CHANGED
|
@@ -12,6 +12,8 @@ from pytbox.win.ad import ADClient
|
|
|
12
12
|
from pytbox.network.meraki import Meraki
|
|
13
13
|
from pytbox.utils.env import get_env_by_os_environment
|
|
14
14
|
from pytbox.vmware import VMwareClient
|
|
15
|
+
from pytbox.pyjira import PyJira
|
|
16
|
+
from pytbox.mail.client import MailClient
|
|
15
17
|
|
|
16
18
|
config = load_config_by_file(path='/workspaces/pytbox/tests/alert/config_dev.toml', oc_vault_id=os.environ.get('oc_vault_id'))
|
|
17
19
|
|
|
@@ -73,4 +75,12 @@ vmware_test = VMwareClient(
|
|
|
73
75
|
password=config['vmware']['test']['password'],
|
|
74
76
|
version=config['vmware']['test']['version'],
|
|
75
77
|
proxies=config['vmware']['test']['proxies']
|
|
76
|
-
)
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
pyjira = PyJira(
|
|
81
|
+
base_url=config['jira']['base_url'],
|
|
82
|
+
token=config['jira']['token']
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
mail_163 = MailClient(mail_address=config['mail']['163']['mail_address'], password=config['mail']['163']['password'])
|
|
86
|
+
mail_qq = MailClient(mail_address=config['mail']['qq']['mail_address'], password=config['mail']['qq']['password'])
|
|
@@ -73,6 +73,16 @@ class VictoriaMetrics:
|
|
|
73
73
|
def check_ping_result(self, target: str, last_minute: int=10, env: str='prod', dev_file: str='') -> ReturnResponse:
|
|
74
74
|
'''
|
|
75
75
|
检查ping结果
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
target (str): 目标地址
|
|
79
|
+
last_minute (int, optional): 最近多少分钟. Defaults to 10.
|
|
80
|
+
env (str, optional): 环境. Defaults to 'prod'.
|
|
81
|
+
dev_file (str, optional): 开发文件. Defaults to ''.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
ReturnResponse:
|
|
85
|
+
code = 0 正常, code = 1 异常, code = 2 没有查询到数据, 建议将其判断为正常
|
|
76
86
|
'''
|
|
77
87
|
if target:
|
|
78
88
|
# 这里需要在字符串中保留 {},同时插入 target,可以用双大括号转义
|
pytbox/excel.py
ADDED
|
File without changes
|
pytbox/feishu/endpoints.py
CHANGED
|
@@ -254,9 +254,9 @@ class MessageEndpoint(Endpoint):
|
|
|
254
254
|
body=payload
|
|
255
255
|
)
|
|
256
256
|
if r.code == 0:
|
|
257
|
-
return ReturnResponse(code=0,
|
|
257
|
+
return ReturnResponse(code=0, msg=f"{message_id} 回复 emoji [{emoji_type}] 成功")
|
|
258
258
|
else:
|
|
259
|
-
return ReturnResponse(code=1,
|
|
259
|
+
return ReturnResponse(code=1, msg=f"{message_id} 回复 emoji [{emoji_type}] 失败")
|
|
260
260
|
|
|
261
261
|
class BitableEndpoint(Endpoint):
|
|
262
262
|
|
|
@@ -432,10 +432,10 @@ class BitableEndpoint(Endpoint):
|
|
|
432
432
|
resp = self.parent.request(path=f'/bitable/v1/apps/{app_token}/tables/{table_id}/records/{record_id}',
|
|
433
433
|
method='PUT',
|
|
434
434
|
body=payload)
|
|
435
|
-
return ReturnResponse(code=resp.code,
|
|
435
|
+
return ReturnResponse(code=resp.code, msg=f"记录已存在, 进行更新", data=resp.data)
|
|
436
436
|
else:
|
|
437
437
|
resp = self.add_record(app_token, table_id, fields)
|
|
438
|
-
return ReturnResponse(code=resp.code,
|
|
438
|
+
return ReturnResponse(code=resp.code, msg=f"记录不存在, 进行创建", data=resp.data)
|
|
439
439
|
|
|
440
440
|
def query_name_by_record_id(self, app_token: str=None, table_id: str=None, field_names: list=None, record_id: str='', name: str=''):
|
|
441
441
|
response = self.query_record(app_token=app_token, table_id=table_id, field_names=field_names)
|
|
@@ -967,9 +967,9 @@ class ExtensionsEndpoint(Endpoint):
|
|
|
967
967
|
body=payload)
|
|
968
968
|
if response.code == 0:
|
|
969
969
|
if get == 'open_id':
|
|
970
|
-
return ReturnResponse(code=0,
|
|
970
|
+
return ReturnResponse(code=0, msg=f'根据用户输入的 {user_input}, 获取用户信息成功', data=response.data['user_list'][0]['user_id'])
|
|
971
971
|
else:
|
|
972
|
-
return ReturnResponse(code=response.code,
|
|
972
|
+
return ReturnResponse(code=response.code, msg=f"获取时失败, 报错请见 data 字段", data=response.data)
|
|
973
973
|
|
|
974
974
|
def format_rich_text(self, text: str, color: Literal['red', 'green', 'yellow', 'blue'], bold: bool=False):
|
|
975
975
|
if bold:
|
pytbox/mail/client.py
CHANGED
|
@@ -1,45 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
from
|
|
5
|
-
from typing import Generator, Literal
|
|
6
|
-
|
|
4
|
+
from contextlib import contextmanager
|
|
7
5
|
import yagmail
|
|
8
|
-
from
|
|
9
|
-
from
|
|
10
|
-
from
|
|
11
|
-
|
|
12
|
-
from src.library.onepassword import my1p
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
log = get_logger('src.library.mail.client')
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
@dataclass
|
|
19
|
-
class MailDetail:
|
|
20
|
-
"""
|
|
21
|
-
邮件详情数据类。
|
|
22
|
-
|
|
23
|
-
Attributes:
|
|
24
|
-
uid: 邮件唯一标识符
|
|
25
|
-
send_from: 发件人邮箱地址
|
|
26
|
-
send_to: 收件人邮箱地址列表
|
|
27
|
-
cc: 抄送人邮箱地址列表
|
|
28
|
-
subject: 邮件主题
|
|
29
|
-
body_plain: 纯文本正文
|
|
30
|
-
body_html: HTML格式正文
|
|
31
|
-
attachment: 附件完整保存路径列表
|
|
32
|
-
"""
|
|
33
|
-
uid: str=None
|
|
34
|
-
send_from: str=None
|
|
35
|
-
send_to: list=None
|
|
36
|
-
date: str=None
|
|
37
|
-
cc: list=None
|
|
38
|
-
subject: str=None
|
|
39
|
-
body_plain: str=None
|
|
40
|
-
body_html: str=None
|
|
41
|
-
attachment: list=None
|
|
42
|
-
has_attachments: bool=False
|
|
6
|
+
from imap_tools import MailBox, MailMessageFlags, AND
|
|
7
|
+
from .mail_detail import MailDetail
|
|
8
|
+
from ..utils.response import ReturnResponse
|
|
43
9
|
|
|
44
10
|
|
|
45
11
|
class MailClient:
|
|
@@ -47,114 +13,93 @@ class MailClient:
|
|
|
47
13
|
_summary_
|
|
48
14
|
'''
|
|
49
15
|
def __init__(self,
|
|
50
|
-
|
|
51
|
-
authorization_code: bool=False,
|
|
16
|
+
mail_address: str=None,
|
|
52
17
|
password: str=None
|
|
53
18
|
):
|
|
54
19
|
|
|
55
|
-
self.mail_address =
|
|
56
|
-
|
|
57
|
-
if password:
|
|
58
|
-
self.password = password
|
|
59
|
-
else:
|
|
60
|
-
if authorization_code:
|
|
61
|
-
self.password = my1p.get_item_by_title(title=send_mail_address)['授权码']
|
|
62
|
-
else:
|
|
63
|
-
self.password = my1p.get_item_by_title(title=send_mail_address)['password']
|
|
20
|
+
self.mail_address = mail_address
|
|
21
|
+
self.password = password
|
|
64
22
|
|
|
65
|
-
if '163.com' in
|
|
23
|
+
if '163.com' in mail_address:
|
|
66
24
|
self.smtp_address = 'smtp.163.com'
|
|
67
25
|
self.imap_address = 'imap.163.com'
|
|
68
|
-
self.imbox_client = self._create_imbox_object(
|
|
69
|
-
elif 'foxmail.com' in
|
|
26
|
+
# self.imbox_client = self._create_imbox_object()
|
|
27
|
+
elif 'foxmail.com' in mail_address:
|
|
70
28
|
self.smtp_address = 'smtp.qq.com'
|
|
71
29
|
self.imap_address = 'imap.qq.com'
|
|
72
|
-
|
|
30
|
+
|
|
73
31
|
else:
|
|
74
|
-
raise ValueError(f'不支持的邮箱地址: {
|
|
75
|
-
|
|
76
|
-
|
|
32
|
+
raise ValueError(f'不支持的邮箱地址: {mail_address}')
|
|
33
|
+
|
|
34
|
+
@contextmanager
|
|
35
|
+
def get_mailbox(self, readonly=False):
|
|
36
|
+
"""
|
|
37
|
+
创建并返回一个已登录的 MailBox 上下文管理器。
|
|
38
|
+
|
|
39
|
+
使用方式:
|
|
40
|
+
with self.get_mailbox() as mailbox:
|
|
41
|
+
# 使用 mailbox 进行操作
|
|
42
|
+
pass
|
|
77
43
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
44
|
+
Args:
|
|
45
|
+
readonly (bool): 是否以只读模式打开邮箱,只读模式下不会将邮件标记为已读
|
|
46
|
+
|
|
47
|
+
Yields:
|
|
48
|
+
MailBox: 已登录的邮箱对象
|
|
49
|
+
"""
|
|
50
|
+
mailbox = MailBox(self.imap_address).login(self.mail_address, self.password)
|
|
51
|
+
if readonly:
|
|
52
|
+
# 以只读模式选择收件箱,防止邮件被标记为已读
|
|
53
|
+
mailbox.folder.set('INBOX', readonly=True)
|
|
54
|
+
try:
|
|
55
|
+
yield mailbox
|
|
56
|
+
finally:
|
|
57
|
+
mailbox.logout()
|
|
85
58
|
|
|
86
|
-
def send_mail(self, receiver: list=
|
|
59
|
+
def send_mail(self, receiver: list=None, cc: list=None, subject: str='', contents: str='', attachments: list=None, tips: str=None):
|
|
87
60
|
'''
|
|
88
61
|
_summary_
|
|
89
62
|
|
|
90
63
|
Args:
|
|
91
|
-
receiver (list, optional): _description_. Defaults to
|
|
92
|
-
cc (list, optional): _description_. Defaults to
|
|
64
|
+
receiver (list, optional): _description_. Defaults to None.
|
|
65
|
+
cc (list, optional): _description_. Defaults to None.
|
|
93
66
|
subject (str, optional): _description_. Defaults to ''.
|
|
94
67
|
contents (str, optional): _description_. Defaults to ''.
|
|
95
|
-
attachments (list, optional): _description_. Defaults to
|
|
68
|
+
attachments (list, optional): _description_. Defaults to None.
|
|
96
69
|
'''
|
|
97
|
-
|
|
98
|
-
————————————————————————————————————————
|
|
99
|
-
以专业成就客户
|
|
100
|
-
钛信(上海)信息科技有限公司
|
|
101
|
-
Tai Xin(ShangHai) Information Technology Co.,Ltd.
|
|
102
|
-
中国上海市浦东新区达尔文路88号半岛科技园21栋2楼
|
|
103
|
-
Tel:400-920-0057
|
|
104
|
-
Web:www.tyun.cn
|
|
105
|
-
————————————————————————————————————————
|
|
106
|
-
信息安全声明:本邮件包含信息归发件人所在组织所有,发件人所在组织对该邮件拥有所有权利。请接收者注意保密,未经发件人书面许可,不得向任何第三方组织和个人透露本邮件所含信息的全部或部分。以上声明仅适用于工作邮件。
|
|
107
|
-
Information Security Notice: The information contained in this mail is solely property of the sender's organization. This mail communication is confidential. Recipients named above are obligated to maintain secrecy and are not permitted to disclose the contents of this communication to others.
|
|
108
|
-
"""
|
|
109
|
-
with yagmail.SMTP(user=self.mail_address, password=my1p.get_item_by_title(self.mail_address)['password'], port=465, host=self.smtp_address) as yag:
|
|
110
|
-
log.info(f'receiver: {receiver}, cc: {cc}, subject: {subject}, contents: {contents}, attachments: {attachments}')
|
|
70
|
+
with yagmail.SMTP(user=self.mail_address, password=self.password, port=465, host=self.smtp_address) as yag:
|
|
111
71
|
try:
|
|
112
72
|
if tips:
|
|
113
|
-
contents = contents + '\n' + '<p style="color: red;"
|
|
114
|
-
|
|
115
|
-
contents = contents + email_signature
|
|
73
|
+
contents = contents + '\n' + '<p style="color: red;">本邮件为系统自动发送</p>'
|
|
116
74
|
yag.send(to=receiver, cc=cc, subject=subject, contents=contents, attachments=attachments)
|
|
117
|
-
log.info('发送成功!!!')
|
|
118
75
|
return True
|
|
119
76
|
except Exception as e:
|
|
120
|
-
log.error(f'发送失败, 报错: {e}')
|
|
121
77
|
return False
|
|
78
|
+
|
|
79
|
+
def get_mail_list(self, seen: bool=False, readonly: bool=True):
|
|
80
|
+
'''
|
|
81
|
+
获取邮件
|
|
122
82
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
# 处理附件
|
|
127
|
-
attachment_list = []
|
|
128
|
-
for att in msg.attachments:
|
|
129
|
-
att_dict = {}
|
|
130
|
-
att_dict['filename'] = att.filename
|
|
131
|
-
att_dict['payload'] = att.payload
|
|
132
|
-
att_dict['size'] = att.size
|
|
133
|
-
att_dict['content_id'] = att.content_id
|
|
134
|
-
att_dict['content_type'] = att.content_type
|
|
135
|
-
att_dict['content_disposition'] = att.content_disposition
|
|
136
|
-
# att_dict['part'] = att.part
|
|
137
|
-
att_dict['size'] = att.size
|
|
138
|
-
attachment_list.append(att_dict)
|
|
139
|
-
|
|
140
|
-
if attachment_list:
|
|
141
|
-
is_has_attachments = True
|
|
142
|
-
else:
|
|
143
|
-
is_has_attachments = False
|
|
83
|
+
Args:
|
|
84
|
+
seen (bool, optional): 默认获取未读邮件, 如果为 True, 则获取已读邮件
|
|
85
|
+
readonly (bool, optional): 是否以只读模式获取邮件,默认为 True,防止邮件被标记为已读
|
|
144
86
|
|
|
87
|
+
Yields:
|
|
88
|
+
MailDetail: 邮件详情
|
|
89
|
+
'''
|
|
90
|
+
with self.get_mailbox(readonly=readonly) as mailbox:
|
|
91
|
+
for msg in mailbox.fetch(AND(seen=seen)):
|
|
145
92
|
yield MailDetail(
|
|
146
93
|
uid=msg.uid,
|
|
147
|
-
|
|
148
|
-
|
|
94
|
+
sent_from=msg.from_,
|
|
95
|
+
sent_to=msg.to,
|
|
149
96
|
date=msg.date,
|
|
150
97
|
cc=msg.cc,
|
|
151
98
|
subject=msg.subject,
|
|
152
99
|
body_plain=msg.text,
|
|
153
|
-
body_html=msg.html
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
)
|
|
157
|
-
|
|
100
|
+
body_html=msg.html
|
|
101
|
+
)
|
|
102
|
+
|
|
158
103
|
def mark_as_read(self, uid):
|
|
159
104
|
"""
|
|
160
105
|
标记邮件为已读。
|
|
@@ -163,13 +108,12 @@ Information Security Notice: The information contained in this mail is solely pr
|
|
|
163
108
|
uid (str): 邮件的唯一标识符
|
|
164
109
|
"""
|
|
165
110
|
try:
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
111
|
+
with self.get_mailbox() as mailbox:
|
|
112
|
+
# 使用 imap_tools 的 flag 方法标记邮件为已读
|
|
113
|
+
# 第一个参数是 uid,第二个参数是要设置的标志,第三个参数 True 表示添加标志
|
|
114
|
+
mailbox.flag(uid, [MailMessageFlags.SEEN], True)
|
|
170
115
|
except Exception as e:
|
|
171
|
-
|
|
172
|
-
raise
|
|
116
|
+
return ReturnResponse(code=1, msg='邮件删除失败', data=e)
|
|
173
117
|
|
|
174
118
|
def delete(self, uid):
|
|
175
119
|
"""
|
|
@@ -179,14 +123,15 @@ Information Security Notice: The information contained in this mail is solely pr
|
|
|
179
123
|
uid (str): 邮件的唯一标识符
|
|
180
124
|
"""
|
|
181
125
|
try:
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
126
|
+
with self.get_mailbox() as mailbox:
|
|
127
|
+
# 使用 imap_tools 的 delete 方法删除邮件
|
|
128
|
+
mailbox.delete(uid)
|
|
129
|
+
# log.info(f'删除邮件{uid}')
|
|
185
130
|
except Exception as e:
|
|
186
|
-
log.error(f'删除邮件{uid}失败: {e}')
|
|
131
|
+
# log.error(f'删除邮件{uid}失败: {e}')
|
|
187
132
|
raise
|
|
188
133
|
|
|
189
|
-
def move(self, uid: str, destination_folder: str) ->
|
|
134
|
+
def move(self, uid: str, destination_folder: str) -> ReturnResponse:
|
|
190
135
|
"""
|
|
191
136
|
移动邮件到指定文件夹。
|
|
192
137
|
|
|
@@ -198,24 +143,25 @@ Information Security Notice: The information contained in this mail is solely pr
|
|
|
198
143
|
destination_folder (str): 目标文件夹名称。
|
|
199
144
|
|
|
200
145
|
Returns:
|
|
201
|
-
|
|
146
|
+
ReturnResponse: 移动邮件结果
|
|
202
147
|
|
|
203
148
|
Raises:
|
|
204
149
|
Exception: 移动过程中底层 imap 库抛出的异常。
|
|
205
150
|
"""
|
|
206
151
|
try:
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
152
|
+
with self.get_mailbox() as mailbox:
|
|
153
|
+
# 使用 imap_tools 的 move 方法移动邮件
|
|
154
|
+
mailbox.move(uid, destination_folder)
|
|
155
|
+
return ReturnResponse(code=0, msg=f'邮件 {uid} 移动到 {destination_folder} 成功', data=None)
|
|
210
156
|
except Exception as e:
|
|
211
|
-
|
|
212
|
-
|
|
157
|
+
return ReturnResponse(code=1, msg=f'邮件 {uid} 移动到 {destination_folder} 失败', data=e)
|
|
158
|
+
|
|
159
|
+
def get_folder_list(self):
|
|
160
|
+
'''
|
|
161
|
+
获取文件夹列表
|
|
162
|
+
'''
|
|
163
|
+
with self.get_mailbox() as mailbox:
|
|
164
|
+
return ReturnResponse(code=0, msg='获取文件夹列表成功', data=mailbox.folder.list())
|
|
213
165
|
|
|
214
166
|
if __name__ == '__main__':
|
|
215
|
-
|
|
216
|
-
# mail = MailClient(send_mail_address='houmingming@tyun.cn')
|
|
217
|
-
# mail = MailClient(send_mail_address='houmdream@163.com', authorization_code=True)
|
|
218
|
-
# mail = MailClient(send_mail_address='houm01@foxmail.com', password=my1p.get_item_by_title('QQ'))
|
|
219
|
-
# 对于阿里云邮箱,使用兼容方法
|
|
220
|
-
pass
|
|
221
|
-
# mail.get_mail_list(attachment=True, attachment_path='/tmp')
|
|
167
|
+
pass
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class MailDetail:
|
|
8
|
+
"""
|
|
9
|
+
邮件详情数据类。
|
|
10
|
+
|
|
11
|
+
Attributes:
|
|
12
|
+
uid: 邮件唯一标识符
|
|
13
|
+
send_from: 发件人邮箱地址
|
|
14
|
+
send_to: 收件人邮箱地址列表
|
|
15
|
+
cc: 抄送人邮箱地址列表
|
|
16
|
+
subject: 邮件主题
|
|
17
|
+
body_plain: 纯文本正文
|
|
18
|
+
body_html: HTML格式正文
|
|
19
|
+
attachment: 附件完整保存路径列表
|
|
20
|
+
"""
|
|
21
|
+
uid: str=None
|
|
22
|
+
sent_from: str=None
|
|
23
|
+
sent_to: list=None
|
|
24
|
+
date: str=None
|
|
25
|
+
cc: list=None
|
|
26
|
+
subject: str=None
|
|
27
|
+
body_plain: str=None
|
|
28
|
+
body_html: str=None
|
|
29
|
+
attachment: list=None
|
|
30
|
+
has_attachments: bool=False
|
pytbox/pyjira.py
ADDED
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Literal, Optional, List
|
|
5
|
+
|
|
6
|
+
from requests.auth import HTTPBasicAuth
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
from .utils.response import ReturnResponse
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PyJira:
|
|
13
|
+
"""JIRA API客户端类
|
|
14
|
+
|
|
15
|
+
提供JIRA操作的封装方法,支持任务创建、更新、查询等功能。
|
|
16
|
+
建议使用OAuth 2.0认证以提高安全性。
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self,
|
|
20
|
+
base_url: str=None,
|
|
21
|
+
proxies: dict=None,
|
|
22
|
+
token: str=None,
|
|
23
|
+
username: str=None,
|
|
24
|
+
password: str=None,
|
|
25
|
+
timeout: int=10
|
|
26
|
+
) -> None:
|
|
27
|
+
'''
|
|
28
|
+
_summary_
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
base_url (str, optional): _description_. Defaults to None.
|
|
32
|
+
proxies (dict, optional): _description_. Defaults to None.
|
|
33
|
+
token (str, optional): _description_. Defaults to None.
|
|
34
|
+
username (str, optional): _description_. Defaults to None.
|
|
35
|
+
password (str, optional): _description_. Defaults to None.
|
|
36
|
+
'''
|
|
37
|
+
self.base_url = base_url
|
|
38
|
+
self.rest_version = 3
|
|
39
|
+
self.proxies = None
|
|
40
|
+
|
|
41
|
+
if 'atlassian.net' in self.base_url:
|
|
42
|
+
self.deploy_type = 'cloud'
|
|
43
|
+
self.rest_version = 3
|
|
44
|
+
else:
|
|
45
|
+
self.deploy_type = 'datacenter'
|
|
46
|
+
self.rest_version = 2
|
|
47
|
+
|
|
48
|
+
if self.deploy_type == 'cloud':
|
|
49
|
+
self.headers = {
|
|
50
|
+
"Accept": "application/json",
|
|
51
|
+
"Content-Type": "application/json",
|
|
52
|
+
"Authorization": f"Basic {token}"
|
|
53
|
+
}
|
|
54
|
+
else:
|
|
55
|
+
self.headers = {
|
|
56
|
+
"Accept": "application/json",
|
|
57
|
+
"Content-Type": "application/json",
|
|
58
|
+
"Authorization": f"Bearer {token}"
|
|
59
|
+
}
|
|
60
|
+
self.auth = HTTPBasicAuth(username, password)
|
|
61
|
+
|
|
62
|
+
self.timeout = timeout
|
|
63
|
+
# 使用 requests.Session 统一管理连接与 headers(不改变现有硬编码与参数传递逻辑)
|
|
64
|
+
self.session = requests.Session()
|
|
65
|
+
self.session.headers.update(self.headers)
|
|
66
|
+
|
|
67
|
+
def get_project_boards(self, project_key: Literal['TEST'] = 'TEST') -> list:
|
|
68
|
+
"""获取指定项目的看板列表
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
project_key: 项目key,例如 'OPS'
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
list: 看板列表
|
|
75
|
+
"""
|
|
76
|
+
boards = self.jira.boards(projectKeyOrID=project_key)
|
|
77
|
+
for board in boards:
|
|
78
|
+
print(board.name)
|
|
79
|
+
return boards
|
|
80
|
+
|
|
81
|
+
def issue_get(self, issue_id_or_key: str, account_id: bool = False) -> ReturnResponse:
|
|
82
|
+
"""获取指定JIRA任务
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
issue_key: 任务key,例如 'TEST-50'
|
|
86
|
+
account_id: 是否返回账户ID,默认为False
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
dict: 任务信息或账户ID
|
|
90
|
+
"""
|
|
91
|
+
url = f"{self.base_url}/rest/api/{self.rest_version}/issue/{issue_id_or_key}"
|
|
92
|
+
r = self.session.get(url, headers=self.headers, timeout=self.timeout)
|
|
93
|
+
|
|
94
|
+
# 移除所有 key 以 customfield_ 开头且后面跟数字的字段
|
|
95
|
+
fields = r.json()['fields']
|
|
96
|
+
keys_to_remove = [k for k in fields if k.startswith('customfield_') and k[12:].isdigit()]
|
|
97
|
+
for k in keys_to_remove:
|
|
98
|
+
fields.pop(k)
|
|
99
|
+
|
|
100
|
+
if r.status_code == 200:
|
|
101
|
+
return ReturnResponse(code=0, msg=f'获取 issue [{issue_id_or_key}] 成功', data=fields)
|
|
102
|
+
else:
|
|
103
|
+
return ReturnResponse(code=1, msg=f'获取 issue [{issue_id_or_key}] 失败')
|
|
104
|
+
|
|
105
|
+
def issue_get_by_key(self, issue_id_or_key:str='', get_key: Literal['assignee', 'summary', 'description', 'status']=None):
|
|
106
|
+
'''
|
|
107
|
+
_summary_
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
issue_id_or_key (str, optional): _description_. Defaults to ''.
|
|
111
|
+
get_key: status 就是 transitions
|
|
112
|
+
|
|
113
|
+
Raises:
|
|
114
|
+
ValueError: _description_
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
_type_: _description_
|
|
118
|
+
'''
|
|
119
|
+
r = self.issue_get(issue_id_or_key=issue_id_or_key)
|
|
120
|
+
if r.code == 0:
|
|
121
|
+
if get_key:
|
|
122
|
+
# print(get_key)
|
|
123
|
+
# print(r.data['assignee'])
|
|
124
|
+
try:
|
|
125
|
+
return r.data[get_key]
|
|
126
|
+
except KeyError as e:
|
|
127
|
+
return r.data
|
|
128
|
+
else:
|
|
129
|
+
raise ValueError(f"{issue_id_or_key} 的 {get_key} 未查找到")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def issue_create(self,
|
|
133
|
+
project_id: int = 10000,
|
|
134
|
+
summary: Optional[str] = None,
|
|
135
|
+
description: str = None,
|
|
136
|
+
description_adf: bool=True,
|
|
137
|
+
issue_type: Literal['任务'] = '任务',
|
|
138
|
+
priority: Literal['Highest', 'High', 'Medium', 'Low', 'Lowest'] = 'Medium',
|
|
139
|
+
reporter: dict=None,
|
|
140
|
+
assignee: dict={},
|
|
141
|
+
parent_key: Optional[str] = None) -> ReturnResponse:
|
|
142
|
+
"""创建一个新的JIRA任务
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
project_id: 项目ID,默认为 10000
|
|
146
|
+
summary: 任务标题
|
|
147
|
+
description: 任务描述,默认为空字符串,支持普通文本,会自动转换为ADF格式
|
|
148
|
+
issue_type: 任务类型,默认为 'Task'
|
|
149
|
+
priority: 优先级,默认为 'Medium'
|
|
150
|
+
parent_key: 父任务key,如果提供则创建子任务,默认为None
|
|
151
|
+
reporter(dict): { "name": reporter}, : {"id" : xx}
|
|
152
|
+
assignee: {"accountId": "xxx"}
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Response: 创建结果,包含任务信息或错误信息
|
|
156
|
+
"""
|
|
157
|
+
url = f"{self.base_url}/rest/api/{self.rest_version}/issue"
|
|
158
|
+
data = {
|
|
159
|
+
"fields": {
|
|
160
|
+
'project': { "id": project_id},
|
|
161
|
+
'summary': summary,
|
|
162
|
+
'issuetype': { "name": issue_type},
|
|
163
|
+
'priority': { "name": priority},
|
|
164
|
+
# 'reporter': reporter
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if assignee:
|
|
168
|
+
data['fields']['assignee'] = assignee
|
|
169
|
+
if description:
|
|
170
|
+
# 将普通文本转换为ADF格式
|
|
171
|
+
if description_adf:
|
|
172
|
+
data['fields']['description'] = self.text_to_adf(description)
|
|
173
|
+
else:
|
|
174
|
+
data['fields']['description'] = description
|
|
175
|
+
|
|
176
|
+
if parent_key:
|
|
177
|
+
data['fields']['parent'] = {"key": parent_key}
|
|
178
|
+
|
|
179
|
+
if reporter:
|
|
180
|
+
if isinstance(reporter, str):
|
|
181
|
+
data['fields']['reporter'] = {"name": reporter}
|
|
182
|
+
else:
|
|
183
|
+
data['fields']['reporter'] = reporter
|
|
184
|
+
|
|
185
|
+
r = self.session.post(
|
|
186
|
+
url,
|
|
187
|
+
headers=self.headers,
|
|
188
|
+
json=data,
|
|
189
|
+
timeout=self.timeout,
|
|
190
|
+
proxies=self.proxies
|
|
191
|
+
)
|
|
192
|
+
if r.status_code == 201:
|
|
193
|
+
return ReturnResponse(code=0, msg=f'创建 issue [{summary}] 成功', data=r.json())
|
|
194
|
+
else:
|
|
195
|
+
return ReturnResponse(code=1, msg=f'创建 issue [{summary}] 失败, {r.text}')
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def issue_update(self,
|
|
199
|
+
issue_key: str,
|
|
200
|
+
summary: Optional[str] = None,
|
|
201
|
+
description: str = None,
|
|
202
|
+
issue_type: str = None,
|
|
203
|
+
priority: Literal['Blocker', 'Critical', 'Major', 'Minor'] = None,
|
|
204
|
+
labels: Optional[list[str]] = None,
|
|
205
|
+
parent_key: str=None) -> dict:
|
|
206
|
+
"""更新JIRA任务
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
issue_key: 任务key,例如 'TEST-50'
|
|
210
|
+
summary: 任务标题,默认为None
|
|
211
|
+
description: 任务描述,默认为空字符串,支持普通文本,会自动转换为ADF格式
|
|
212
|
+
issue_type: 任务类型,默认为 'Task'
|
|
213
|
+
priority: 优先级,默认为 'Minor'
|
|
214
|
+
assignee: 分配人,默认为 'MingMing Hou'
|
|
215
|
+
status: 任务状态,默认为None
|
|
216
|
+
labels: 标签列表,默认为None
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
dict: 更新后的任务信息或错误信息
|
|
220
|
+
"""
|
|
221
|
+
url = f"{self.base_url}/rest/api/{self.rest_version}/issue/{issue_key}"
|
|
222
|
+
data = { "fields": {} }
|
|
223
|
+
if summary:
|
|
224
|
+
data['fields']['summary'] = summary
|
|
225
|
+
if description is not None:
|
|
226
|
+
description = self.format_description(description)
|
|
227
|
+
# 将普通文本转换为ADF格式
|
|
228
|
+
data['fields']['description'] = self.text_to_adf(description)
|
|
229
|
+
if issue_type:
|
|
230
|
+
data['fields']['issuetype'] = issue_type
|
|
231
|
+
if priority:
|
|
232
|
+
data['fields']['priority'] = {"name": priority}
|
|
233
|
+
if labels:
|
|
234
|
+
data['fields']['labels'] = labels
|
|
235
|
+
|
|
236
|
+
if parent_key:
|
|
237
|
+
data['fields']['parent'] = {"key": parent_key}
|
|
238
|
+
|
|
239
|
+
# print(data)
|
|
240
|
+
r = self.session.put(url, headers=self.headers, json=data, timeout=self.timeout)
|
|
241
|
+
if r.status_code == 204:
|
|
242
|
+
return ReturnResponse(code=0, msg=f'更新 issue [{issue_key}] 成功', data=r.text)
|
|
243
|
+
else:
|
|
244
|
+
try:
|
|
245
|
+
error_data = r.json()
|
|
246
|
+
error_message = f'更新 issue [{issue_key}] 失败, status: {r.status_code}, 错误: {error_data}'
|
|
247
|
+
except:
|
|
248
|
+
error_message = f'更新 issue [{issue_key}] 失败, status: {r.status_code}, 响应: {r.text}'
|
|
249
|
+
return ReturnResponse(code=1, msg=error_message, data=r.text)
|
|
250
|
+
|
|
251
|
+
def issue_assign(self, issue_id_or_key: str='', name: str=None, display_name: str=None, account_id: str=None) -> ReturnResponse:
|
|
252
|
+
'''
|
|
253
|
+
_summary_
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
issue_id_or_key (str, optional): _description_. Defaults to ''.
|
|
257
|
+
name (str, optional): _description_. Defaults to None.
|
|
258
|
+
display_name (str, optional): _description_. Defaults to None.
|
|
259
|
+
account_id (str, optional): _description_. 侯明明: 612dbc1d1dbcd90069240013 包柿林: 712020:66dfa4a9-383e-4aa2-abdd-521adfccf967
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
ReturnResponse: _description_
|
|
263
|
+
'''
|
|
264
|
+
update = False
|
|
265
|
+
url = f"{self.base_url}/rest/api/{self.rest_version}/issue/{issue_id_or_key}/assignee"
|
|
266
|
+
data = {}
|
|
267
|
+
if name:
|
|
268
|
+
data["name"] = name
|
|
269
|
+
assignee_message = name
|
|
270
|
+
update = True
|
|
271
|
+
|
|
272
|
+
if display_name:
|
|
273
|
+
data["displayName"] = display_name
|
|
274
|
+
assignee_message = display_name
|
|
275
|
+
update = True
|
|
276
|
+
|
|
277
|
+
if account_id:
|
|
278
|
+
account_info = self.issue_get_by_key(issue_id_or_key=issue_id_or_key, get_key='assignee')
|
|
279
|
+
if account_info is None:
|
|
280
|
+
update = True
|
|
281
|
+
else:
|
|
282
|
+
current_account_id = account_info['accountId']
|
|
283
|
+
if current_account_id != account_id:
|
|
284
|
+
update = True
|
|
285
|
+
|
|
286
|
+
data["accountId"] = account_id
|
|
287
|
+
assignee_message = account_id
|
|
288
|
+
|
|
289
|
+
if update:
|
|
290
|
+
r = self.session.put(url, headers=self.headers, json=data, timeout=self.timeout)
|
|
291
|
+
if r.status_code == 204:
|
|
292
|
+
return ReturnResponse(code=0, msg=f'更新 issue [{issue_id_or_key}] 的分配人成功, 经办人被更新为 {assignee_message}', data=r.text)
|
|
293
|
+
return ReturnResponse(code=1, msg=f'更新 issue [{issue_id_or_key}] 的分配人失败, status code: {r.status_code}, 报错: {r.text}')
|
|
294
|
+
return ReturnResponse(code=0, msg=f'issue [{issue_id_or_key}] 的分配人已经为 {assignee_message}, 不需要更新')
|
|
295
|
+
|
|
296
|
+
def issue_comment_add(self, issue_key: str, comment: str) -> dict:
|
|
297
|
+
"""添加JIRA任务评论
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
issue_key: 任务key,例如 'TEST-50'
|
|
301
|
+
comment: 评论内容
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
dict: 评论信息
|
|
305
|
+
"""
|
|
306
|
+
url = f"{self.base_url}/rest/api/{self.rest_version}/issue/{issue_key}/comment/"
|
|
307
|
+
comment_adf = self.text_to_adf(text=self.format_description(comment))
|
|
308
|
+
r = self.session.post(url, headers=self.headers, json={"body": comment_adf}, timeout=self.timeout)
|
|
309
|
+
if r.status_code == 201:
|
|
310
|
+
return ReturnResponse(code=0, msg=f'添加评论 [{comment}] 成功', data=r.json())
|
|
311
|
+
else:
|
|
312
|
+
return ReturnResponse(code=1, msg=f'添加评论 [{comment}] 失败, 返回值: {r.text}', data=r.json())
|
|
313
|
+
|
|
314
|
+
def issue_search(self, jql: str, max_results: int = 50, fields: Optional[List[str]] = None) -> ReturnResponse:
|
|
315
|
+
"""使用JQL搜索JIRA任务
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
jql: JQL查询字符串
|
|
319
|
+
max_results: 最大返回结果数,默认50
|
|
320
|
+
fields: 需要返回的字段列表,默认返回所有字段
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
ReturnResponse: 包含任务列表的响应数据
|
|
324
|
+
"""
|
|
325
|
+
url = f"{self.base_url}/rest/api/3/search"
|
|
326
|
+
|
|
327
|
+
# 构建请求体
|
|
328
|
+
payload = {
|
|
329
|
+
"jql": jql,
|
|
330
|
+
"maxResults": max_results,
|
|
331
|
+
"startAt": 0
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
# 如果指定了字段,添加到请求体中
|
|
335
|
+
if fields:
|
|
336
|
+
payload["fields"] = fields
|
|
337
|
+
|
|
338
|
+
r = self.session.post(url, headers=self.headers, json=payload, timeout=self.timeout)
|
|
339
|
+
|
|
340
|
+
if r.status_code == 200:
|
|
341
|
+
return ReturnResponse(code=0, msg='', data=r.json())
|
|
342
|
+
else:
|
|
343
|
+
return ReturnResponse(code=1, msg=f'获取 issue 失败, status code: {r.status_code}, 报错: {r.text}')
|
|
344
|
+
|
|
345
|
+
def get_boards(self) -> ReturnResponse:
|
|
346
|
+
"""获取所有看板信息并打印
|
|
347
|
+
|
|
348
|
+
使用REST API获取看板信息,主要用于调试和查看。
|
|
349
|
+
"""
|
|
350
|
+
url = f"{self.base_url}/rest/agile/1.0/board"
|
|
351
|
+
response = self.session.get(url, headers=self.headers, timeout=self.timeout)
|
|
352
|
+
response.raise_for_status()
|
|
353
|
+
return ReturnResponse(code=0, msg='', data=response.json())
|
|
354
|
+
|
|
355
|
+
def get_issue_fields(self) -> ReturnResponse:
|
|
356
|
+
url = f"{self.base_url}/rest/api/3/field"
|
|
357
|
+
response = self.session.get(url, headers=self.headers, timeout=self.timeout)
|
|
358
|
+
response.raise_for_status()
|
|
359
|
+
return ReturnResponse(code=0, msg='', data=response.json())
|
|
360
|
+
|
|
361
|
+
def get_project(self, project_id_or_key: Literal['TEST']='TEST') -> ReturnResponse:
|
|
362
|
+
if self.deploy_type == 'cloud':
|
|
363
|
+
url = f"{self.base_url}/rest/api/3/project/{project_id_or_key}"
|
|
364
|
+
else:
|
|
365
|
+
url = f"{self.base_url}/rest/api/2/project"
|
|
366
|
+
|
|
367
|
+
response = self.session.get(url, headers=self.headers, timeout=self.timeout)
|
|
368
|
+
if response.status_code == 401:
|
|
369
|
+
return ReturnResponse(code=1, msg=response.text)
|
|
370
|
+
else:
|
|
371
|
+
return ReturnResponse(code=0, msg='', data=response.json())
|
|
372
|
+
|
|
373
|
+
# def get_issue(self, issue_id_or_key: str) -> ReturnResponse:
|
|
374
|
+
# url = f"{self.base_url}/rest/api/{self.rest_version}/issue/{issue_id_or_key}"
|
|
375
|
+
# response = self.session.get(url, headers=self.headers, timeout=self.timeout)
|
|
376
|
+
# if response.status_code == 200:
|
|
377
|
+
# return ReturnResponse(code=0, msg=f'根据输入的 {issue_id_or_key} 成功获取到 issue', data=response.json())
|
|
378
|
+
# else:
|
|
379
|
+
# return ReturnResponse(code=1, msg=response.text)
|
|
380
|
+
|
|
381
|
+
def get_metadata_for_project_issue_types(self, project_id_or_key: Literal['TEST', 'MO', '10000']='TEST'):
|
|
382
|
+
url = f"{self.base_url}/rest/api/2/issue/createmeta/{project_id_or_key}/issuetypes"
|
|
383
|
+
response = self.session.get(url, headers=self.headers, timeout=self.timeout)
|
|
384
|
+
if response.status_code == 200:
|
|
385
|
+
return ReturnResponse(code=0, msg='', data=response.json())
|
|
386
|
+
else:
|
|
387
|
+
return ReturnResponse(code=1, msg=f"{response.status_code}: {response.text}")
|
|
388
|
+
|
|
389
|
+
def get_metadata_for_issue_type_used_for_create_issue(self, project_id_or_key: Literal['TEST', 'MO']='TEST', issue_type_id: Literal['10000']='10000'):
|
|
390
|
+
url = f"{self.base_url}/rest/api/{self.rest_version}/issue/createmeta/{project_id_or_key}/issuetypes/{issue_type_id}"
|
|
391
|
+
r = self.session.get(url, headers=self.headers, timeout=self.timeout)
|
|
392
|
+
return r
|
|
393
|
+
|
|
394
|
+
def get_user(self, username: str=None, key: str=None):
|
|
395
|
+
url = f"{self.base_url}/rest/api/{self.rest_version}/user"
|
|
396
|
+
r = self.session.get(url, headers=self.headers, params={'username': username}, timeout=self.timeout)
|
|
397
|
+
return r
|
|
398
|
+
|
|
399
|
+
def get_issue_property(self, issue_id_or_key):
|
|
400
|
+
url = f"{self.base_url}/rest/api/2/issue/{issue_id_or_key}/properties"
|
|
401
|
+
r = self.session.get(url, headers=self.headers, timeout=self.timeout)
|
|
402
|
+
return r
|
|
403
|
+
|
|
404
|
+
def find_user(self, query: str) -> ReturnResponse:
|
|
405
|
+
'''
|
|
406
|
+
查找用户
|
|
407
|
+
|
|
408
|
+
Args:
|
|
409
|
+
query (str): 建议输入用户的邮箱
|
|
410
|
+
|
|
411
|
+
Returns:
|
|
412
|
+
_type_:
|
|
413
|
+
'''
|
|
414
|
+
url = f"{self.base_url}/rest/api/{self.rest_version}/user/search"
|
|
415
|
+
r = self.session.get(url, headers=self.headers, params={'query': query}, timeout=self.timeout)
|
|
416
|
+
if r.status_code == 200:
|
|
417
|
+
return ReturnResponse(code=0, msg=f'查找到 {len(r.json())} 个用户', data=r.json()[0])
|
|
418
|
+
else:
|
|
419
|
+
return ReturnResponse(code=1, msg=f'获取用户失败, status code: {r.status_code}, 报错: {r.text}')
|
|
420
|
+
|
|
421
|
+
def get_issue_transitions(self, issue_id_or_key) -> ReturnResponse:
|
|
422
|
+
url = f"{self.base_url}/rest/api/{self.rest_version}/issue/{issue_id_or_key}/transitions"
|
|
423
|
+
r = self.session.get(url, headers=self.headers, timeout=self.timeout)
|
|
424
|
+
if r.status_code == 200:
|
|
425
|
+
return ReturnResponse(code=0, msg='', data=r.json())
|
|
426
|
+
else:
|
|
427
|
+
return ReturnResponse(code=1, msg=f'获取 issue [{issue_id_or_key}] 的 transitions 失败, status code: {r.status_code}, 报错: {r.text}')
|
|
428
|
+
|
|
429
|
+
def get_issue_transitions_by_name(self, issue_id_or_key, name) -> ReturnResponse:
|
|
430
|
+
r = self.get_issue_transitions(issue_id_or_key=issue_id_or_key)
|
|
431
|
+
for transition in r.data['transitions']:
|
|
432
|
+
if transition['name'] == name:
|
|
433
|
+
return ReturnResponse(code=0, msg=f'获取 issue [{issue_id_or_key}] 的 transitions 成功, 返回值: {transition["id"]}', data=transition['id'])
|
|
434
|
+
raise ValueError(f'获取 issue [{issue_id_or_key}] 的 transitions 失败, 没有找到状态为 {name} 的 transition')
|
|
435
|
+
|
|
436
|
+
def issue_transition(self, issue_id_or_key, transition_name) -> ReturnResponse:
|
|
437
|
+
update = False
|
|
438
|
+
status = self.issue_get_by_key(issue_id_or_key=issue_id_or_key, get_key='status')
|
|
439
|
+
if status:
|
|
440
|
+
if transition_name != status['name']:
|
|
441
|
+
update = True
|
|
442
|
+
if update:
|
|
443
|
+
url = f"{self.base_url}/rest/api/{self.rest_version}/issue/{issue_id_or_key}/transitions"
|
|
444
|
+
data = {
|
|
445
|
+
"transition": {
|
|
446
|
+
"id": self.get_issue_transitions_by_name(issue_id_or_key=issue_id_or_key, name=transition_name).data
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
r = self.session.post(url, headers=self.headers, json=data, timeout=self.timeout)
|
|
450
|
+
if r.status_code == 204:
|
|
451
|
+
return ReturnResponse(code=0, msg=f'更新 issue [{issue_id_or_key}] 的状态成功, 状态被更新为 {transition_name}', data=r.text)
|
|
452
|
+
else:
|
|
453
|
+
return ReturnResponse(code=1, msg=f'更新 issue [{issue_id_or_key}] 的状态失败, status code: {r.status_code}, 报错: {r.text}')
|
|
454
|
+
else:
|
|
455
|
+
return ReturnResponse(code=0, msg=f'issue [{issue_id_or_key}] 的状态已经为 {status["name"]}, 不需要更新')
|
|
456
|
+
|
|
457
|
+
def format_description(self, description):
|
|
458
|
+
'''
|
|
459
|
+
格式化描述
|
|
460
|
+
'''
|
|
461
|
+
if not description:
|
|
462
|
+
return ""
|
|
463
|
+
|
|
464
|
+
new_description = description
|
|
465
|
+
|
|
466
|
+
# 内容清理正则表达式
|
|
467
|
+
CONTENT_CLEANUP_PATTERNS = [
|
|
468
|
+
r'\*\*工作历时\*\*: \d+小时',
|
|
469
|
+
r'\*\*工作历时\*\*:.*小时',
|
|
470
|
+
r'!\[image\]\(.*?\)',
|
|
471
|
+
r'!\[file\]\(.*?\)',
|
|
472
|
+
r'\[Jira Link\]\(.*?\)',
|
|
473
|
+
r'bear://x-callback-url/open-note\?id=.*'
|
|
474
|
+
]
|
|
475
|
+
|
|
476
|
+
for pattern in CONTENT_CLEANUP_PATTERNS:
|
|
477
|
+
new_description = re.sub(pattern, '', new_description)
|
|
478
|
+
return new_description.strip()
|
|
479
|
+
|
|
480
|
+
def text_to_adf(self, text: str) -> dict:
|
|
481
|
+
"""
|
|
482
|
+
将普通文本转换为Atlassian Document Format (ADF)格式。
|
|
483
|
+
|
|
484
|
+
Args:
|
|
485
|
+
text: 要转换的文本内容
|
|
486
|
+
|
|
487
|
+
Returns:
|
|
488
|
+
dict: ADF格式的文档对象
|
|
489
|
+
"""
|
|
490
|
+
if not text:
|
|
491
|
+
return {
|
|
492
|
+
"type": "doc",
|
|
493
|
+
"version": 1,
|
|
494
|
+
"content": [
|
|
495
|
+
{
|
|
496
|
+
"type": "paragraph",
|
|
497
|
+
"content": [
|
|
498
|
+
{
|
|
499
|
+
"type": "text",
|
|
500
|
+
"text": ""
|
|
501
|
+
}
|
|
502
|
+
]
|
|
503
|
+
}
|
|
504
|
+
]
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
# 将文本按行分割
|
|
508
|
+
lines = text.split('\n')
|
|
509
|
+
content = []
|
|
510
|
+
|
|
511
|
+
for line in lines:
|
|
512
|
+
if line.strip(): # 非空行
|
|
513
|
+
paragraph_content = [
|
|
514
|
+
{
|
|
515
|
+
"type": "text",
|
|
516
|
+
"text": line
|
|
517
|
+
}
|
|
518
|
+
]
|
|
519
|
+
content.append({
|
|
520
|
+
"type": "paragraph",
|
|
521
|
+
"content": paragraph_content
|
|
522
|
+
})
|
|
523
|
+
else: # 空行,添加换行符
|
|
524
|
+
if content: # 如果前面有内容,添加换行
|
|
525
|
+
content.append({
|
|
526
|
+
"type": "paragraph",
|
|
527
|
+
"content": [
|
|
528
|
+
{
|
|
529
|
+
"type": "text",
|
|
530
|
+
"text": ""
|
|
531
|
+
}
|
|
532
|
+
]
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
# 如果没有内容,至少添加一个空段落
|
|
536
|
+
if not content:
|
|
537
|
+
content = [
|
|
538
|
+
{
|
|
539
|
+
"type": "paragraph",
|
|
540
|
+
"content": [
|
|
541
|
+
{
|
|
542
|
+
"type": "text",
|
|
543
|
+
"text": ""
|
|
544
|
+
}
|
|
545
|
+
]
|
|
546
|
+
}
|
|
547
|
+
]
|
|
548
|
+
|
|
549
|
+
return {
|
|
550
|
+
"type": "doc",
|
|
551
|
+
"version": 1,
|
|
552
|
+
"content": content
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
if __name__ == '__main__':
|
|
557
|
+
pass
|
|
558
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytbox
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: A collection of Python integrations and utilities (Feishu, Dida365, VictoriaMetrics, ...)
|
|
5
5
|
Author-email: mingming hou <houm01@foxmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -16,6 +16,8 @@ Requires-Dist: rich>=12.0.0
|
|
|
16
16
|
Requires-Dist: jinja2>=3.0.0
|
|
17
17
|
Requires-Dist: toml>=0.10.0
|
|
18
18
|
Requires-Dist: ldap3>=2.9.1
|
|
19
|
+
Requires-Dist: imap-tools>=1.11.0
|
|
20
|
+
Requires-Dist: yagmail>=0.15.293
|
|
19
21
|
Provides-Extra: dev
|
|
20
22
|
Requires-Dist: pytest; extra == "dev"
|
|
21
23
|
Requires-Dist: black; extra == "dev"
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
pytbox/base.py,sha256=
|
|
1
|
+
pytbox/base.py,sha256=XGl2d87FLVp8ILhFF4P_ZVTZGSzxejV3PoyKUp4UCkQ,2937
|
|
2
2
|
pytbox/cli.py,sha256=N775a0GK80IT2lQC2KRYtkZpIiu9UjavZmaxgNUgJhQ,160
|
|
3
3
|
pytbox/dida365.py,sha256=pUMPB9AyLZpTTbaz2LbtzdEpyjvuGf4YlRrCvM5sbJo,10545
|
|
4
|
+
pytbox/excel.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
5
|
pytbox/onepassword_connect.py,sha256=nD3xTl1ykQ4ct_dCRRF138gXCtk-phPfKYXuOn-P7Z8,3064
|
|
5
6
|
pytbox/onepassword_sa.py,sha256=08iUcYud3aEHuQcUsem9bWNxdXKgaxFbMy9yvtr-DZQ,6995
|
|
7
|
+
pytbox/pyjira.py,sha256=TMy34Rtu7OXRA8wpUuLsFeyIQfRNUn2ed2no00L8YSE,22470
|
|
6
8
|
pytbox/vmware.py,sha256=WiH67_3-VCBjXJuh3UueOc31BdZDItiZhkeuPzoRhw4,3975
|
|
7
9
|
pytbox/alert/alert_handler.py,sha256=FePPQS4LyGphSJ0QMv0_pLWaXxEqsRlcTKMfUjtsNfk,5048
|
|
8
10
|
pytbox/alert/ping.py,sha256=g36X0U3U8ndZqfpVIcuoxJJ0X5gST3I_IwjTQC1roHA,779
|
|
@@ -41,16 +43,17 @@ pytbox/cli/formatters/__init__.py,sha256=4o85w4j-A-O1oBLvuE9q8AFiJ2C9rvB3MIKsy5V
|
|
|
41
43
|
pytbox/cli/formatters/output.py,sha256=h5WhZlQk1rjmxEj88Jy5ODLcv6L5zfGUhks_3AWIkKU,5455
|
|
42
44
|
pytbox/common/__init__.py,sha256=3JWfgCQZKZuSH5NCE7OCzKwq82pkyop9l7sH5YSNyfU,122
|
|
43
45
|
pytbox/database/mongo.py,sha256=CSpHC7iR-M0BaVxXz5j6iXjMKPgXJX_G7MrjCj5Gm8Q,3478
|
|
44
|
-
pytbox/database/victoriametrics.py,sha256=
|
|
46
|
+
pytbox/database/victoriametrics.py,sha256=wTTsCOMpUpia8Bhp3clpFrUo3ow0T822MnZMxJnXRVM,5392
|
|
45
47
|
pytbox/feishu/client.py,sha256=kwGLseGT_iQUFmSqpuS2_77WmxtHstD64nXvktuQ3B4,5865
|
|
46
|
-
pytbox/feishu/endpoints.py,sha256=
|
|
48
|
+
pytbox/feishu/endpoints.py,sha256=z3nPOZPC2JGDJlO7SusWBpRA33hZZ4Z-GBhI6F8L_u4,40240
|
|
47
49
|
pytbox/feishu/errors.py,sha256=79qFAHZw7jDj3gnWAjI1-W4tB0q1_aSfdjee4xzXeuI,1179
|
|
48
50
|
pytbox/feishu/helpers.py,sha256=jhSkHiUw4822QBXx2Jw8AksogZdakZ-3QqvC3lB3qEI,201
|
|
49
51
|
pytbox/feishu/typing.py,sha256=3hWkJgOi-v2bt9viMxkyvNHsPgrbAa0aZOxsZYg2vdM,122
|
|
50
52
|
pytbox/log/logger.py,sha256=7ZisXRxLb_MVbIqlYHWoTbj1EA0Z4G5SZvITlt1IKW8,7416
|
|
51
53
|
pytbox/log/victorialog.py,sha256=gffEiq38adv9sC5oZeMcyKghd3SGfRuqtZOFuqHQF6E,4139
|
|
52
54
|
pytbox/mail/alimail.py,sha256=ap6K6kmKTjqbHlfecYBi-EZtOY1iQ8tCilP8oqUz21Q,4621
|
|
53
|
-
pytbox/mail/client.py,sha256=
|
|
55
|
+
pytbox/mail/client.py,sha256=6HeKpChHGjTCYGBgQcfAhWlU_wh9wtO-bjP6TU38pGM,6120
|
|
56
|
+
pytbox/mail/mail_detail.py,sha256=6u8DK-7WzYPSuX6TdicSCh2Os_9Ou6Rn9xc6WRvv85M,699
|
|
54
57
|
pytbox/network/meraki.py,sha256=054E3C5KzAuXs9aPalvdAOUo6Hc5aOKZSWUaVbPquy4,6112
|
|
55
58
|
pytbox/utils/env.py,sha256=gD2-NyL3K3Vg1B1eGeD1hRtlSHPGgF8Oi9mchuQL6_o,646
|
|
56
59
|
pytbox/utils/load_config.py,sha256=R4pGerBinbewsym41hQ8Z-I5I7gepuEKODjIrli4C08,5043
|
|
@@ -60,8 +63,8 @@ pytbox/utils/response.py,sha256=kXjlwt0WVmLRam2eu1shzX2cQ7ux4cCQryaPGYwle5g,1247
|
|
|
60
63
|
pytbox/utils/richutils.py,sha256=OT9_q2Q1bthzB0g1GlhZVvM4ZAepJRKL6a_Vsr6vEqo,487
|
|
61
64
|
pytbox/utils/timeutils.py,sha256=XbK2KB-SVi7agNqoQN7i40wysrZvrGuwebViv1Cw-Ok,20226
|
|
62
65
|
pytbox/win/ad.py,sha256=-3pWfL3dElz-XoO4j4M9lrgu3KJtlhrS9gCWJBpafAU,1147
|
|
63
|
-
pytbox-0.1.
|
|
64
|
-
pytbox-0.1.
|
|
65
|
-
pytbox-0.1.
|
|
66
|
-
pytbox-0.1.
|
|
67
|
-
pytbox-0.1.
|
|
66
|
+
pytbox-0.1.3.dist-info/METADATA,sha256=go1IkU9GPVADsgEj5_yqgU5ueyLpU66-82AHS6S0JGw,6319
|
|
67
|
+
pytbox-0.1.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
68
|
+
pytbox-0.1.3.dist-info/entry_points.txt,sha256=YaTOJ2oPjOiv2SZwY0UC-UA9QS2phRH1oMvxGnxO0Js,43
|
|
69
|
+
pytbox-0.1.3.dist-info/top_level.txt,sha256=YADgWue-Oe128ptN3J2hS3GB0Ncc5uZaSUM3e1rwswE,7
|
|
70
|
+
pytbox-0.1.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|