pytbox 0.2.4__py3-none-any.whl → 0.2.6__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.
pytbox/base.py CHANGED
@@ -16,6 +16,8 @@ from pytbox.mail.client import MailClient
16
16
  from pytbox.mail.alimail import AliMail
17
17
  from pytbox.alicloud.sls import AliCloudSls
18
18
  from pytbox.utils.cronjob import cronjob_counter
19
+ from pytbox.notion import Notion
20
+ from pytbox.mingdao import Mingdao
19
21
 
20
22
 
21
23
  config = load_config_by_file(path='/workspaces/pytbox/tests/alert/config_dev.toml', oc_vault_id=os.environ.get('oc_vault_id'))
@@ -96,6 +98,7 @@ vmware_test = VMwareClient(
96
98
 
97
99
  pyjira = PyJira(
98
100
  base_url=config['jira']['base_url'],
101
+ username=config['jira']['username'],
99
102
  token=config['jira']['token']
100
103
  )
101
104
 
@@ -111,4 +114,8 @@ sls = AliCloudSls(
111
114
  )
112
115
 
113
116
  def get_cronjob_counter(app_type='', app='', comment=None, schedule_interval=None, schedule_cron=None):
114
- return cronjob_counter(vm=vm, log=get_logger('cronjob_counter'), app_type=app_type, app=app, comment=comment, schedule_interval=schedule_interval, schedule_cron=schedule_cron)
117
+ return cronjob_counter(vm=vm, log=get_logger('cronjob_counter'), app_type=app_type, app=app, comment=comment, schedule_interval=schedule_interval, schedule_cron=schedule_cron)
118
+
119
+
120
+ notion = Notion(token=config['notion']['api_secrets'], proxy=config['notion']['proxy'])
121
+ mingdao = Mingdao(app_key=config['mingdao']['app_key'], sign=config['mingdao']['sign'])
@@ -168,41 +168,19 @@ class VictoriaMetrics:
168
168
  ReturnResponse:
169
169
  code = 0 正常, code = 1 异常, code = 2 没有查询到数据, 建议将其判断为正常
170
170
  '''
171
- if target:
172
- # 这里需要在字符串中保留 {},同时插入 target,可以用双大括号转义
173
- query = f"ping_result_code{{target='{target}'}}"
174
- else:
175
- query = "ping_result_code"
176
-
177
- if last_minute:
178
- query = query + f"[{last_minute}m]"
179
-
180
- if env == 'dev':
171
+ query = f'avg_over_time((ping_result_code{{target="{target}"}})[{last_minute}m])'
172
+ if self.env == 'dev':
181
173
  r = load_dev_file(dev_file)
182
174
  else:
183
175
  r = self.query(query=query)
184
-
185
176
  if r.code == 0:
186
- values = r.data[0]['values']
187
- if len(values) == 2 and values[1] == "0":
188
- code = 0
189
- msg = f"已检查 {target} 最近 {last_minute} 分钟是正常的!"
177
+ value = r.data[0]['value'][1]
178
+ if value == '0':
179
+ return ReturnResponse(code=0, msg=f"已检查 {target} 最近 {last_minute} 分钟是正常的!", data=r.data)
190
180
  else:
191
- if all(str(item[1]) == "1" for item in values):
192
- code = 1
193
- msg = f"已检查 {target} 最近 {last_minute} 分钟是异常的!"
194
- else:
195
- code = 0
196
- msg = f"已检查 {target} 最近 {last_minute} 分钟是正常的!"
197
- elif r.code == 2:
198
- code = 2
199
- msg = f"没有查询到 {target} 最近 {last_minute} 分钟的ping结果!"
200
- try:
201
- data = r.data[0]
202
- except KeyError:
203
- data = r.data
204
-
205
- return ReturnResponse(code=code, msg=msg, data=data)
181
+ return ReturnResponse(code=1, msg=f"已检查 {target} 最近 {last_minute} 分钟是异常的!", data=r.data)
182
+ else:
183
+ return r
206
184
 
207
185
  def check_unreachable_ping_result(self, dev_file: str='') -> ReturnResponse:
208
186
  '''
@@ -396,4 +374,30 @@ class VictoriaMetrics:
396
374
  labels['schedule_type'] = 'cron'
397
375
  labels['schedule_cron'] = schedule_cron
398
376
  r = self.insert(metric_name="cronjob_run_duration_seconds", labels=labels, value=duration_seconds)
399
- return r
377
+ return r
378
+
379
+ def get_vmware_esxhostnames(self, vcenter: str=None) -> list:
380
+ '''
381
+ _summary_
382
+ '''
383
+ esxhostnames = []
384
+ query = f'vsphere_host_sys_uptime_latest{{vcenter="{vcenter}"}}'
385
+ metrics = self.query(query=query).data
386
+ for metric in metrics:
387
+ esxhostname = metric['metric']['esxhostname']
388
+ esxhostnames.append(esxhostname)
389
+ return esxhostnames
390
+
391
+ def get_vmware_cpu_usage(self, vcenter: str=None, esxhostname: str=None) -> float:
392
+ '''
393
+ _summary_
394
+ '''
395
+ query = f'vsphere_host_cpu_usage_average{{vcenter="{vcenter}", esxhostname="{esxhostname}"}}'
396
+ return self.query(query=query).data[0]['value'][1]
397
+
398
+ def get_vmware_memory_usage(self, vcenter: str=None, esxhostname: str=None) -> float:
399
+ '''
400
+ _summary_
401
+ '''
402
+ query = f'vsphere_host_mem_usage_average{{vcenter="{vcenter}", esxhostname="{esxhostname}"}}'
403
+ return self.query(query=query).data[0]['value'][1]
pytbox/mingdao.py ADDED
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env python3
2
+
3
+ from typing import Literal, Type
4
+
5
+ import requests
6
+
7
+ from .utils.response import ReturnResponse
8
+
9
+
10
+ class Mingdao:
11
+ '''
12
+ _summary_
13
+ '''
14
+ def __init__(self, app_key: str=None, sign: str=None, timeout: int=5):
15
+ self.base_url = "https://api.mingdao.com"
16
+ self.headers = {
17
+ 'Content-Type': 'application/json;charset=UTF-8',
18
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36'
19
+ }
20
+ self.timeout = timeout
21
+ self.app_key = app_key
22
+ self.sign = sign
23
+
24
+ def _build_api_request(self, api_url: str, method: Literal['GET', 'POST'], params: dict=None, body: dict=None, api_version: Literal['v1', 'v2']='v2'):
25
+ body['appKey'] = self.app_key
26
+ body['sign'] = self.sign
27
+ if not api_url.startswith('/'):
28
+ url = f'{self.base_url}/{api_version}/{api_url}'
29
+ else:
30
+ url = f'{self.base_url}/{api_version}{api_url}'
31
+ params = {
32
+ "appKey": self.app_key,
33
+ "sign": self.sign,
34
+ }
35
+ return requests.request(method, url, params=params, headers=self.headers, json=body, timeout=self.timeout)
36
+
37
+ def get_app_info(self) -> ReturnResponse:
38
+ '''
39
+ _summary_
40
+
41
+ Returns:
42
+ ReturnResponse: _description_
43
+ '''
44
+ r = self._build_api_request(api_url='/open/app/get', method='GET', body={}, api_version='v1')
45
+ return ReturnResponse(code=0, msg='获取应用信息成功', data=r.json())
46
+
47
+ def get_work_sheet_info(self, worksheet_id: str=None, table_name: str=None, worksheet_name: str=None):
48
+ if worksheet_name:
49
+ worksheet_id = self.get_work_sheet_id_by_name(table_name=table_name, worksheet_name=worksheet_name)
50
+
51
+ r = self._build_api_request(
52
+ api_url='open/worksheet/getWorksheetInfo',
53
+ method='POST',
54
+ body={
55
+ "worksheetId": worksheet_id,
56
+ }
57
+ )
58
+ return r.json()
59
+
60
+ def get_project_info(self, worksheet_id: str, keywords: str):
61
+ r = self._build_api_request(
62
+ api_url='open/worksheet/getFilterRows',
63
+ method='POST',
64
+ body={
65
+ "pageIndex": 1,
66
+ "pageSize": 100,
67
+ "worksheetId": worksheet_id,
68
+ "keyWords": keywords,
69
+ }
70
+ )
71
+ return r.json()
72
+
73
+ def get_work_sheet_id_by_name(self, table_name: str, worksheet_name: str, child_section: bool=False):
74
+ r = self.get_app_info()
75
+ for i in r.data['data']['sections']:
76
+ if table_name == i['name']:
77
+ if child_section:
78
+ for child in i['childSections'][0]['items']:
79
+ if child['name'] == worksheet_name:
80
+ return child['id']
81
+ else:
82
+ for item in i['items']:
83
+ if item['name'] == worksheet_name:
84
+ return item['id']
85
+
86
+ def get_control_id(self, table_name: str=None, worksheet_name: str=None, control_name: str=None):
87
+ r = self.get_work_sheet_info(table_name=table_name, worksheet_name=worksheet_name)
88
+ for control in r['data']['controls']:
89
+ if control['controlName'] == control_name:
90
+ return control['controlId']
91
+ return None
92
+
93
+ def get_value(self, table_name: str=None, worksheet_name: str=None, control_name: str=None, value_name: str=None):
94
+ control_id = self.get_control_id(table_name=table_name, worksheet_name=worksheet_name, control_name=control_name)
95
+
96
+ r = self._build_api_request(
97
+ api_url='open/worksheet/getFilterRows',
98
+ method='POST',
99
+ body={
100
+ "pageIndex": 1,
101
+ "pageSize": 100,
102
+ "worksheetId": self.get_work_sheet_id_by_name(table_name=table_name, worksheet_name=worksheet_name),
103
+ # "filters": filters
104
+ }
105
+ )
106
+ for row in r.json()['data']['rows']:
107
+ if row[control_id] == value_name:
108
+ return row['rowid']
109
+
110
+ def get_work_record(self,
111
+ worksheet_id: str=None,
112
+ project_control_id: str=None,
113
+ project_value: str=None,
114
+ complete_date_control_id: str=None,
115
+ complete_date: Literal['Today', '上个月', 'Last7Day', 'Last30Day']=None,
116
+ parse_control_id: bool=False,
117
+ page_size: int=100,
118
+ ):
119
+
120
+ filters = []
121
+ if project_value:
122
+ filters.append({
123
+ "controlId": project_control_id,
124
+ "dataType": 29,
125
+ "spliceType": 1,
126
+ "filterType": 24,
127
+ "dateRange": 0,
128
+ "dateRangeType": 0,
129
+ "value": "",
130
+ "values": [
131
+ project_value
132
+ ],
133
+ "minValue": "",
134
+ "maxValue": ""
135
+ })
136
+
137
+ if complete_date:
138
+ if complete_date == '上个月':
139
+ data_range = 8
140
+ elif complete_date == 'Today':
141
+ data_range = 1
142
+ elif complete_date == 'Last7Day':
143
+ data_range = 21
144
+ else:
145
+ data_range = 1
146
+
147
+ filters.append({
148
+ "controlId": complete_date_control_id,
149
+ "dataType": 15,
150
+ "spliceType": 1,
151
+ "filterType": 17,
152
+ "dateRange": data_range,
153
+ })
154
+ r = self._build_api_request(
155
+ api_url='open/worksheet/getFilterRows',
156
+ method='POST',
157
+ body={
158
+ "pageIndex": 1,
159
+ "pageSize": page_size,
160
+ "worksheetId": worksheet_id,
161
+ "filters": filters
162
+ }
163
+ )
164
+ return r.json()
pytbox/notion.py ADDED
@@ -0,0 +1,731 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import requests
4
+ from .utils.response import ReturnResponse
5
+
6
+
7
+ class Notion:
8
+ '''
9
+ Notion API 简单封装类
10
+ '''
11
+ def __init__(self, token: str, proxy: str = None, timeout: int=10):
12
+ self.token = token
13
+ self.headers = {
14
+ "accept": "application/json",
15
+ "Notion-Version": "2022-06-28",
16
+ "content-type": "application/json",
17
+ "Authorization": f"Bearer {self.token}"
18
+ }
19
+ self.timeout = timeout
20
+ self.proxy = proxy
21
+ self.session = requests.Session()
22
+ self.session.headers.update(self.headers)
23
+ self.session.proxies.update({"http": self.proxy, "https": self.proxy})
24
+ self.base_url = "https://api.notion.com/v1"
25
+
26
+ def database_create(self, page_id: str, name: str = "test databse") -> ReturnResponse:
27
+ """
28
+ 在指定的 page 下创建一个简单的数据库
29
+
30
+ Args:
31
+ page_id: 父页面的 ID(不需要带横杠)
32
+ database_name: 数据库名称
33
+
34
+ Returns:
35
+ dict: API 响应结果
36
+ """
37
+ url = f"{self.base_url}/databases"
38
+
39
+ payload = {
40
+ "parent": {
41
+ "type": "page_id",
42
+ "page_id": page_id
43
+ },
44
+ "title": [
45
+ {
46
+ "type": "text",
47
+ "text": {
48
+ "content": name
49
+ }
50
+ }
51
+ ],
52
+ "properties": {
53
+ "名称": {
54
+ "title": {}
55
+ }
56
+ }
57
+ }
58
+
59
+ response = self.session.post(url, json=payload, timeout=self.timeout)
60
+
61
+ if response.status_code == 200:
62
+ return ReturnResponse(code=0, msg=f"数据库创建成功: {name}", data=response.json())
63
+ else:
64
+ return ReturnResponse(code=1, msg=f"数据库创建失败: {response.status_code}", data=response.text)
65
+
66
+ def database_update(self, db_id: str, title: str = None, description: str = None, icon: dict = None, cover: dict = None) -> ReturnResponse:
67
+ """
68
+ 更新数据库属性
69
+
70
+ Args:
71
+ db_id: 数据库 ID
72
+ title: 数据库标题
73
+ description: 数据库描述
74
+ icon: 数据库图标
75
+ cover: 数据库封面
76
+
77
+ Returns:
78
+ ReturnResponse: 更新结果
79
+ """
80
+ url = f"{self.base_url}/databases/{db_id}"
81
+
82
+ payload = {}
83
+
84
+ # 构建更新字段
85
+ if title is not None:
86
+ payload["title"] = [
87
+ {
88
+ "type": "text",
89
+ "text": {
90
+ "content": title
91
+ }
92
+ }
93
+ ]
94
+
95
+ if description is not None:
96
+ payload["description"] = [
97
+ {
98
+ "type": "text",
99
+ "text": {
100
+ "content": description
101
+ }
102
+ }
103
+ ]
104
+
105
+ if icon is not None:
106
+ payload["icon"] = icon
107
+
108
+ if cover is not None:
109
+ payload["cover"] = cover
110
+
111
+ # 如果没有提供任何更新字段,返回错误
112
+ if not payload:
113
+ return ReturnResponse(code=1, msg="至少需要提供一个更新字段", data=None)
114
+
115
+ response = self.session.patch(url, json=payload, timeout=self.timeout)
116
+
117
+ if response.status_code == 200:
118
+ return ReturnResponse(code=0, msg="数据库更新成功", data=response.json())
119
+ else:
120
+ return ReturnResponse(code=1, msg=f"数据库更新失败: {response.status_code}", data=response.text)
121
+
122
+ def page_create(self, db_id: str, properties: dict = None) -> ReturnResponse:
123
+ """
124
+ 在数据库中创建新页面(添加数据)
125
+
126
+ Args:
127
+ db_id: 数据库 ID
128
+ properties: 页面属性字典,键为属性名,值为属性值
129
+
130
+ Returns:
131
+ ReturnResponse: 创建结果
132
+ """
133
+ url = f"{self.base_url}/pages"
134
+
135
+ # 默认属性
136
+ if properties is None:
137
+ properties = {}
138
+
139
+ # 构建请求体
140
+ payload = {
141
+ "parent": {
142
+ "type": "database_id",
143
+ "database_id": db_id
144
+ },
145
+ "properties": {}
146
+ }
147
+
148
+ # 处理属性
149
+ for key, value in properties.items():
150
+ payload["properties"][key] = self._format_property_value(key, value)
151
+
152
+ response = self.session.post(url, json=payload, timeout=self.timeout)
153
+
154
+ if response.status_code == 200:
155
+ return ReturnResponse(code=0, msg="页面创建成功", data=response.json())
156
+ else:
157
+ return ReturnResponse(code=1, msg=f"页面创建失败: {response.status_code}", data=response.text)
158
+
159
+ def page_update(self, page_id: str, properties: dict = None) -> ReturnResponse:
160
+ """
161
+ 更新页面属性
162
+
163
+ Args:
164
+ page_id: 页面 ID
165
+ properties: 页面属性字典
166
+
167
+ Returns:
168
+ ReturnResponse: 更新结果
169
+ """
170
+ url = f"{self.base_url}/pages/{page_id}"
171
+
172
+ if properties is None:
173
+ properties = {}
174
+
175
+ # 构建请求体
176
+ payload = {"properties": {}}
177
+
178
+ # 处理属性
179
+ for key, value in properties.items():
180
+ payload["properties"][key] = self._format_property_value(key, value)
181
+
182
+ response = self.session.patch(url, json=payload, timeout=self.timeout)
183
+
184
+ if response.status_code == 200:
185
+ return ReturnResponse(code=0, msg="页面更新成功", data=response.json())
186
+ else:
187
+ return ReturnResponse(code=1, msg=f"页面更新失败: {response.status_code}", data=response.text)
188
+
189
+ def page_upsert(self, db_id: str, unique_field: str, unique_value: str, properties: dict = None, content: list = None) -> ReturnResponse:
190
+ """
191
+ 智能创建或更新页面(Upsert操作)
192
+
193
+ 根据唯一字段查找页面:
194
+ - 如果找到匹配的页面,则更新该页面的属性
195
+ - 如果未找到匹配的页面,则创建新页面
196
+
197
+ 支持的属性类型:
198
+ - 标题属性 (title): 字段名包含 name/title/名称/标题
199
+ - 文本属性 (rich_text): 普通文本字段
200
+ - 复选框属性 (checkbox): 布尔值
201
+ - 数字属性 (number): 数字值
202
+ - 多选属性 (multi_select): 字符串列表
203
+ - 单选属性 (select): 字典格式 {"name": "选项名"}
204
+ - 日期属性 (date): 字典格式 {"start": "2024-01-01"}
205
+ - URL属性 (url): 字典格式 {"url": "https://example.com"}
206
+ - 邮箱属性 (email): 字典格式 {"email": "test@example.com"}
207
+ - 电话号码属性 (phone_number): 字典格式 {"phone_number": "123-456-7890"}
208
+ - 状态属性 (status): 字典格式 {"name": "状态名"}
209
+ - 关系属性 (relation): 支持多种格式
210
+ * 直接ID: [{"id": "页面ID"}]
211
+ * 便捷格式: [{"title": "页面标题", "database_id": "数据库ID"}]
212
+ * 混合格式: [{"id": "页面ID1"}, {"title": "页面标题", "database_id": "数据库ID"}]
213
+ - 人员属性 (people): 列表格式 [{"id": "用户ID"}] 或 [{"email": "邮箱"}]
214
+
215
+ Args:
216
+ db_id: 数据库 ID
217
+ unique_field: 用于查找的唯一字段名(如"名称"、"Name"等)
218
+ unique_value: 唯一字段的值,用于查找匹配的页面
219
+ properties: 页面属性字典,包含要设置或更新的属性
220
+ content: 页面内容列表,包含要添加的内容块
221
+
222
+ Returns:
223
+ ReturnResponse: 创建或更新结果
224
+ - code=0: 操作成功
225
+ - code=1: 操作失败,查看 msg 和 data 获取错误信息
226
+
227
+ Example:
228
+ # 第一次调用会创建新页面
229
+ result = notion.page_upsert(
230
+ db_id="database_id",
231
+ unique_field="Name",
232
+ unique_value="项目A",
233
+ properties={
234
+ "Name": "项目A",
235
+ "Status": {"name": "进行中"},
236
+ "Priority": 1,
237
+ "Tags": ["重要", "紧急"],
238
+ # 关系字段便捷用法
239
+ "RelatedPages": [
240
+ {"title": "相关项目1", "database_id": "related_db_id"},
241
+ {"title": "相关项目2", "database_id": "related_db_id"}
242
+ ]
243
+ },
244
+ # 页面内容
245
+ content=[
246
+ {"type": "heading_1", "text": "项目概述"},
247
+ {"type": "paragraph", "text": "这是项目的详细描述"},
248
+ {"type": "to_do", "text": "完成需求分析", "checked": True},
249
+ {"type": "to_do", "text": "设计系统架构", "checked": False}
250
+ ]
251
+ )
252
+
253
+ # 第二次调用会更新现有页面
254
+ result = notion.page_upsert(
255
+ db_id="database_id",
256
+ unique_field="Name",
257
+ unique_value="项目A", # 相同的唯一值
258
+ properties={
259
+ "Name": "项目A",
260
+ "Status": {"name": "已完成"}, # 更新状态
261
+ "Priority": 2, # 更新优先级
262
+ # 混合关系字段格式
263
+ "RelatedPages": [
264
+ {"id": "existing_page_id"}, # 直接ID
265
+ {"title": "新相关项目", "database_id": "related_db_id"} # 便捷格式
266
+ ]
267
+ },
268
+ # 更新页面内容
269
+ content=[
270
+ {"type": "heading_2", "text": "更新内容"},
271
+ {"type": "paragraph", "text": "项目状态已更新"},
272
+ {"type": "quote", "text": "项目进展顺利"}
273
+ ]
274
+ )
275
+ """
276
+ if properties is None:
277
+ properties = {}
278
+ if content is None:
279
+ content = []
280
+
281
+ # 1. 先查询数据库,看是否存在相同记录
282
+ query_url = f"{self.base_url}/databases/{db_id}/query"
283
+ query_payload = {
284
+ "filter": {
285
+ "property": unique_field,
286
+ "title": {
287
+ "equals": unique_value
288
+ }
289
+ }
290
+ }
291
+
292
+ query_response = self.session.post(query_url, json=query_payload, timeout=self.timeout)
293
+
294
+ if query_response.status_code == 200:
295
+ results = query_response.json().get("results", [])
296
+
297
+ if results:
298
+ # 找到记录,执行更新
299
+ page_id = results[0]["id"]
300
+
301
+ # 更新页面属性
302
+ update_result = self.page_update(page_id, properties)
303
+
304
+ # 如果有内容需要添加,则添加内容
305
+ if content and update_result.code == 0:
306
+ content_result = self.page_add_content(page_id, content)
307
+ if content_result.code == 0:
308
+ return content_result
309
+ else:
310
+ return update_result
311
+ else:
312
+ return update_result
313
+ else:
314
+ # 未找到记录,执行创建
315
+ # 确保唯一字段包含在属性中
316
+ if unique_field not in properties:
317
+ properties[unique_field] = unique_value
318
+
319
+ # 创建页面
320
+ create_result = self.page_create(db_id, properties)
321
+
322
+ # 如果有内容需要添加,则添加内容
323
+ if content and create_result.code == 0:
324
+ page_id = create_result.data["id"]
325
+ content_result = self.page_add_content(page_id, content)
326
+ if content_result.code == 0:
327
+ return content_result
328
+ else:
329
+ return create_result
330
+ else:
331
+ return create_result
332
+ else:
333
+ # 查询失败,直接尝试创建
334
+ if unique_field not in properties:
335
+ properties[unique_field] = unique_value
336
+
337
+ # 创建页面
338
+ create_result = self.page_create(db_id, properties)
339
+
340
+ # 如果有内容需要添加,则添加内容
341
+ if content and create_result.code == 0:
342
+ page_id = create_result.data["id"]
343
+ content_result = self.page_add_content(page_id, content)
344
+ if content_result.code == 0:
345
+ return content_result
346
+ else:
347
+ return create_result
348
+ else:
349
+ return create_result
350
+
351
+ def _find_page_by_title(self, db_id: str, title: str) -> str:
352
+ """
353
+ 根据标题在数据库中查找页面ID
354
+
355
+ Args:
356
+ db_id: 数据库ID
357
+ title: 页面标题
358
+
359
+ Returns:
360
+ str: 页面ID,如果未找到返回None
361
+ """
362
+ query_url = f"{self.base_url}/databases/{db_id}/query"
363
+ query_payload = {
364
+ "filter": {
365
+ "property": "Name", # 假设标题字段名为"Name"
366
+ "title": {
367
+ "equals": title
368
+ }
369
+ }
370
+ }
371
+
372
+ response = self.session.post(query_url, json=query_payload, timeout=self.timeout)
373
+
374
+ if response.status_code == 200:
375
+ results = response.json().get("results", [])
376
+ if results:
377
+ return results[0]["id"]
378
+
379
+ return None
380
+
381
+ def _format_relation_value(self, value) -> list:
382
+ """
383
+ 格式化关系属性值,支持多种输入格式
384
+
385
+ Args:
386
+ value: 关系值,支持以下格式:
387
+ - 字符串: 页面标题,需要指定database_id
388
+ - 字典: {"title": "页面标题", "database_id": "数据库ID"}
389
+ - 列表: [{"title": "标题1", "database_id": "数据库ID1"}, ...]
390
+ - 列表: [{"id": "页面ID1"}, {"id": "页面ID2"}]
391
+
392
+ Returns:
393
+ list: 格式化后的关系值列表
394
+ """
395
+ if isinstance(value, str):
396
+ # 字符串格式,需要database_id
397
+ raise ValueError("字符串格式需要同时提供database_id,请使用字典格式:{'title': '页面标题', 'database_id': '数据库ID'}")
398
+
399
+ elif isinstance(value, dict):
400
+ if "id" in value:
401
+ # 直接提供页面ID
402
+ return [{"id": value["id"]}]
403
+ elif "title" in value and "database_id" in value:
404
+ # 提供标题和数据库ID,需要查找页面ID
405
+ page_id = self._find_page_by_title(value["database_id"], value["title"])
406
+ if page_id:
407
+ return [{"id": page_id}]
408
+ else:
409
+ raise ValueError(f"未找到标题为 '{value['title']}' 的页面")
410
+ else:
411
+ raise ValueError("字典格式必须包含 'id' 或 ('title' 和 'database_id')")
412
+
413
+ elif isinstance(value, list):
414
+ relation_result = []
415
+ for item in value:
416
+ if isinstance(item, str):
417
+ raise ValueError("列表中的字符串需要提供database_id")
418
+ elif isinstance(item, dict):
419
+ if "id" in item:
420
+ relation_result.append({"id": item["id"]})
421
+ elif "title" in item and "database_id" in item:
422
+ page_id = self._find_page_by_title(item["database_id"], item["title"])
423
+ if page_id:
424
+ relation_result.append({"id": page_id})
425
+ else:
426
+ raise ValueError(f"未找到标题为 '{item['title']}' 的页面")
427
+ else:
428
+ raise ValueError("列表项必须包含 'id' 或 ('title' 和 'database_id')")
429
+ else:
430
+ raise ValueError("列表项必须是字典格式")
431
+ return relation_result
432
+
433
+ else:
434
+ raise ValueError("不支持的关系值格式")
435
+
436
+ def _format_property_value(self, key: str, value) -> dict:
437
+ """
438
+ 根据属性类型格式化属性值
439
+
440
+ Args:
441
+ key: 属性名
442
+ value: 属性值
443
+
444
+ Returns:
445
+ dict: 格式化后的属性值
446
+ """
447
+ if isinstance(value, str):
448
+ # 根据字段名判断属性类型
449
+ if key.lower() in ['name', 'title', '名称', '标题']:
450
+ # 标题属性
451
+ return {
452
+ "title": [
453
+ {
454
+ "type": "text",
455
+ "text": {
456
+ "content": value
457
+ }
458
+ }
459
+ ]
460
+ }
461
+ else:
462
+ # 文本属性 (rich_text)
463
+ return {
464
+ "rich_text": [
465
+ {
466
+ "type": "text",
467
+ "text": {
468
+ "content": value
469
+ }
470
+ }
471
+ ]
472
+ }
473
+ elif isinstance(value, bool):
474
+ # 复选框属性
475
+ return {"checkbox": value}
476
+ elif isinstance(value, (int, float)):
477
+ # 数字属性
478
+ return {"number": value}
479
+ elif isinstance(value, list):
480
+ # 列表类型,需要进一步判断
481
+ if not value:
482
+ # 空列表,默认作为多选处理
483
+ return {"multi_select": []}
484
+
485
+ # 根据第一个元素的类型判断
486
+ first_item = value[0]
487
+ if isinstance(first_item, str):
488
+ # 多选属性
489
+ return {
490
+ "multi_select": [
491
+ {"name": item} for item in value
492
+ ]
493
+ }
494
+ elif isinstance(first_item, dict):
495
+ # 复杂对象列表
496
+ if "name" in first_item:
497
+ # 多选属性
498
+ return {"multi_select": value}
499
+ elif "id" in first_item or "title" in first_item:
500
+ # 关系属性
501
+ try:
502
+ return {"relation": self._format_relation_value(value)}
503
+ except ValueError:
504
+ return {"relation": value} # 回退到原始值
505
+ elif "email" in first_item:
506
+ # 人员属性
507
+ return {"people": value}
508
+ elif "object" in first_item and first_item["object"] == "user":
509
+ # 人员属性(用户对象格式)
510
+ return {"people": value}
511
+ return {"multi_select": [{"name": str(item)} for item in value]}
512
+ elif isinstance(value, dict):
513
+ # 字典类型
514
+ if "start" in value:
515
+ # 日期属性
516
+ return {"date": value}
517
+ elif "name" in value:
518
+ # 单选属性
519
+ return {"select": value}
520
+ elif "url" in value:
521
+ # URL属性
522
+ return {"url": value["url"]}
523
+ elif "email" in value:
524
+ # 邮箱属性
525
+ return {"email": value["email"]}
526
+ elif "phone_number" in value:
527
+ # 电话号码属性
528
+ return {"phone_number": value["phone_number"]}
529
+ elif "status" in value:
530
+ # 状态属性
531
+ return {"status": value}
532
+ elif "id" in value or "title" in value:
533
+ # 关系属性
534
+ try:
535
+ return {"relation": self._format_relation_value(value)}
536
+ except ValueError:
537
+ return {"relation": value} # 回退到原始值
538
+ else:
539
+ # 默认作为单选处理
540
+ return {"select": value}
541
+ else:
542
+ # 其他类型,转换为字符串处理
543
+ return {
544
+ "rich_text": [
545
+ {
546
+ "type": "text",
547
+ "text": {
548
+ "content": str(value)
549
+ }
550
+ }
551
+ ]
552
+ }
553
+
554
+ def page_add_content(self, page_id: str, content: list) -> ReturnResponse:
555
+ """
556
+ 向页面添加内容块
557
+
558
+ Args:
559
+ page_id: 页面 ID
560
+ content: 内容块列表,每个元素是一个内容块
561
+
562
+ Returns:
563
+ ReturnResponse: 添加结果
564
+ """
565
+ url = f"{self.base_url}/blocks/{page_id}/children"
566
+
567
+ # 格式化内容块
568
+ formatted_blocks = []
569
+ for block in content:
570
+ formatted_blocks.append(self._format_content_block(block))
571
+
572
+ payload = {"children": formatted_blocks}
573
+
574
+ response = self.session.patch(url, json=payload, timeout=self.timeout)
575
+
576
+ if response.status_code == 200:
577
+ return ReturnResponse(code=0, msg="页面内容添加成功", data=response.json())
578
+ else:
579
+ return ReturnResponse(code=1, msg=f"页面内容添加失败: {response.status_code}", data=response.text)
580
+
581
+ def _format_content_block(self, block: dict) -> dict:
582
+ """
583
+ 格式化内容块
584
+
585
+ Args:
586
+ block: 内容块字典,格式如:
587
+ {
588
+ "type": "paragraph",
589
+ "text": "这是段落文本"
590
+ }
591
+
592
+ {
593
+ "type": "heading_1",
594
+ "text": "这是一级标题"
595
+ }
596
+
597
+ Returns:
598
+ dict: 格式化后的内容块
599
+ """
600
+ block_type = block.get("type", "paragraph")
601
+ text = block.get("text", "")
602
+
603
+ # 基础文本内容
604
+ text_content = {
605
+ "type": "text",
606
+ "text": {
607
+ "content": text
608
+ }
609
+ }
610
+
611
+ # 根据类型返回不同的块结构
612
+ if block_type == "paragraph":
613
+ return {
614
+ "type": "paragraph",
615
+ "paragraph": {
616
+ "rich_text": [text_content]
617
+ }
618
+ }
619
+ elif block_type == "heading_1":
620
+ return {
621
+ "type": "heading_1",
622
+ "heading_1": {
623
+ "rich_text": [text_content]
624
+ }
625
+ }
626
+ elif block_type == "heading_2":
627
+ return {
628
+ "type": "heading_2",
629
+ "heading_2": {
630
+ "rich_text": [text_content]
631
+ }
632
+ }
633
+ elif block_type == "heading_3":
634
+ return {
635
+ "type": "heading_3",
636
+ "heading_3": {
637
+ "rich_text": [text_content]
638
+ }
639
+ }
640
+ elif block_type == "bulleted_list_item":
641
+ return {
642
+ "type": "bulleted_list_item",
643
+ "bulleted_list_item": {
644
+ "rich_text": [text_content]
645
+ }
646
+ }
647
+ elif block_type == "numbered_list_item":
648
+ return {
649
+ "type": "numbered_list_item",
650
+ "numbered_list_item": {
651
+ "rich_text": [text_content]
652
+ }
653
+ }
654
+ elif block_type == "to_do":
655
+ checked = block.get("checked", False)
656
+ return {
657
+ "type": "to_do",
658
+ "to_do": {
659
+ "rich_text": [text_content],
660
+ "checked": checked
661
+ }
662
+ }
663
+ elif block_type == "quote":
664
+ return {
665
+ "type": "quote",
666
+ "quote": {
667
+ "rich_text": [text_content]
668
+ }
669
+ }
670
+ elif block_type == "code":
671
+ language = block.get("language", "plain text")
672
+ return {
673
+ "type": "code",
674
+ "code": {
675
+ "rich_text": [text_content],
676
+ "language": language
677
+ }
678
+ }
679
+ else:
680
+ # 默认作为段落处理
681
+ return {
682
+ "type": "paragraph",
683
+ "paragraph": {
684
+ "rich_text": [text_content]
685
+ }
686
+ }
687
+
688
+ if __name__ == "__main__":
689
+ # 使用示例
690
+ notion = Notion(token="your_integration_token_here")
691
+ database_id = "your_database_id_here"
692
+
693
+ # 使用智能创建/更新方法
694
+ # 第一次调用会创建新记录
695
+ result = notion.page_upsert(
696
+ db_id=database_id,
697
+ unique_field="名称", # 用于查找的唯一字段
698
+ unique_value="测试记录", # 唯一字段的值
699
+ properties={
700
+ "名称": "测试记录",
701
+ "状态": ["进行中"],
702
+ "完成": False,
703
+ "优先级": 1
704
+ }
705
+ )
706
+
707
+ # 第二次调用会更新现有记录
708
+ result = notion.page_upsert(
709
+ db_id=database_id,
710
+ unique_field="名称",
711
+ unique_value="测试记录", # 相同的唯一值
712
+ properties={
713
+ "名称": "测试记录",
714
+ "状态": ["已完成"], # 更新状态
715
+ "完成": True, # 更新完成状态
716
+ "优先级": 2 # 更新优先级
717
+ }
718
+ )
719
+
720
+ # 使用不同的唯一值会创建新记录
721
+ result = notion.page_upsert(
722
+ db_id=database_id,
723
+ unique_field="名称",
724
+ unique_value="新记录",
725
+ properties={
726
+ "名称": "新记录",
727
+ "状态": ["待处理"],
728
+ "完成": False,
729
+ "优先级": 3
730
+ }
731
+ )
pytbox/pyjira.py CHANGED
@@ -89,8 +89,11 @@ class PyJira:
89
89
  dict: 任务信息或账户ID
90
90
  """
91
91
  url = f"{self.base_url}/rest/api/{self.rest_version}/issue/{issue_id_or_key}"
92
+ print(url)
92
93
  r = self.session.get(url, headers=self.headers, timeout=self.timeout)
93
94
 
95
+ print(r.text)
96
+
94
97
  # 移除所有 key 以 customfield_ 开头且后面跟数字的字段
95
98
  fields = r.json()['fields']
96
99
  keys_to_remove = [k for k in fields if k.startswith('customfield_') and k[12:].isdigit()]
@@ -313,6 +316,7 @@ class PyJira:
313
316
 
314
317
  def issue_search(self, jql: str, max_results: int = 50, fields: Optional[List[str]] = None) -> ReturnResponse:
315
318
  """使用JQL搜索JIRA任务
319
+ https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-search/#api-rest-api-3-search-jql-get
316
320
 
317
321
  Args:
318
322
  jql: JQL查询字符串
@@ -322,26 +326,29 @@ class PyJira:
322
326
  Returns:
323
327
  ReturnResponse: 包含任务列表的响应数据
324
328
  """
325
- url = f"{self.base_url}/rest/api/3/search"
329
+ url = f"{self.base_url}/rest/api/3/search/jql"
326
330
 
327
- # 构建请求体
328
- payload = {
331
+ # 构建查询参数
332
+ params = {
329
333
  "jql": jql,
330
334
  "maxResults": max_results,
331
335
  "startAt": 0
332
336
  }
333
337
 
334
- # 如果指定了字段,添加到请求体中
335
- if fields:
336
- payload["fields"] = fields
338
+ # 如果指定了字段,添加到查询参数中
339
+ if isinstance(fields, list):
340
+ params["fields"] = ",".join(fields)
341
+ else:
342
+ params["fields"] = fields
337
343
 
338
- r = self.session.post(url, headers=self.headers, json=payload, timeout=self.timeout)
344
+ r = self.session.get(url, headers=self.headers, params=params, timeout=self.timeout)
339
345
 
340
346
  if r.status_code == 200:
341
347
  return ReturnResponse(code=0, msg='', data=r.json())
342
348
  else:
343
349
  return ReturnResponse(code=1, msg=f'获取 issue 失败, status code: {r.status_code}, 报错: {r.text}')
344
350
 
351
+
345
352
  def get_boards(self) -> ReturnResponse:
346
353
  """获取所有看板信息并打印
347
354
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytbox
3
- Version: 0.2.4
3
+ Version: 0.2.6
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
@@ -1,10 +1,12 @@
1
- pytbox/base.py,sha256=VAAaf081rTih9_FjOvAvcihbXp-zwmbGvASnoB2mNZo,4254
1
+ pytbox/base.py,sha256=WC_p_PYMMpNq-5JRLyZoD74k-d2EQVmO4VjTvnI7VqE,4541
2
2
  pytbox/cli.py,sha256=N775a0GK80IT2lQC2KRYtkZpIiu9UjavZmaxgNUgJhQ,160
3
3
  pytbox/dida365.py,sha256=pUMPB9AyLZpTTbaz2LbtzdEpyjvuGf4YlRrCvM5sbJo,10545
4
4
  pytbox/excel.py,sha256=f5XBLCeJbGgxytoSwVhbk03WLTzz8Q3IJ_RZ2-r_w6A,2334
5
+ pytbox/mingdao.py,sha256=afEFJ9NKPdsmAZ4trBEJKl66fMj3Z8TWfaOcomNGhzw,6042
6
+ pytbox/notion.py,sha256=GRPdZAtyG2I6M6pCFbdrTWDACaPsp1RAXrY_RpWYKus,26572
5
7
  pytbox/onepassword_connect.py,sha256=nD3xTl1ykQ4ct_dCRRF138gXCtk-phPfKYXuOn-P7Z8,3064
6
8
  pytbox/onepassword_sa.py,sha256=08iUcYud3aEHuQcUsem9bWNxdXKgaxFbMy9yvtr-DZQ,6995
7
- pytbox/pyjira.py,sha256=TMy34Rtu7OXRA8wpUuLsFeyIQfRNUn2ed2no00L8YSE,22470
9
+ pytbox/pyjira.py,sha256=7yWfu_qsQNoEoQWagnriD9QWrMyTC3OCAvGfvjLZP74,22731
8
10
  pytbox/vmware.py,sha256=WiH67_3-VCBjXJuh3UueOc31BdZDItiZhkeuPzoRhw4,3975
9
11
  pytbox/alert/alert_handler.py,sha256=WCn4cKahv5G5BFGgmc7dX7BQ38h2kxTgxfRVTwc1O2M,6579
10
12
  pytbox/alert/ping.py,sha256=KEnnXdIRJHvR_rEHPWLBt0wz4cGwmA29Lenlak3Z_1Y,778
@@ -43,7 +45,7 @@ pytbox/cli/formatters/__init__.py,sha256=4o85w4j-A-O1oBLvuE9q8AFiJ2C9rvB3MIKsy5V
43
45
  pytbox/cli/formatters/output.py,sha256=h5WhZlQk1rjmxEj88Jy5ODLcv6L5zfGUhks_3AWIkKU,5455
44
46
  pytbox/common/__init__.py,sha256=3JWfgCQZKZuSH5NCE7OCzKwq82pkyop9l7sH5YSNyfU,122
45
47
  pytbox/database/mongo.py,sha256=AhJ9nCAQHKrrcL-ujeonOwEf3x2QkmT2VhoCdglqJmU,3478
46
- pytbox/database/victoriametrics.py,sha256=EqKJag3s-E0V4Xb3vQNc82w6tOth2Q0_0dC15AKOgRs,15344
48
+ pytbox/database/victoriametrics.py,sha256=vUv2sRTvtdIRiZWrbLmAp6mrey8bJKVZc3bRFBpFnok,15660
47
49
  pytbox/feishu/client.py,sha256=kwGLseGT_iQUFmSqpuS2_77WmxtHstD64nXvktuQ3B4,5865
48
50
  pytbox/feishu/endpoints.py,sha256=z3nPOZPC2JGDJlO7SusWBpRA33hZZ4Z-GBhI6F8L_u4,40240
49
51
  pytbox/feishu/errors.py,sha256=79qFAHZw7jDj3gnWAjI1-W4tB0q1_aSfdjee4xzXeuI,1179
@@ -63,8 +65,8 @@ pytbox/utils/response.py,sha256=kXjlwt0WVmLRam2eu1shzX2cQ7ux4cCQryaPGYwle5g,1247
63
65
  pytbox/utils/richutils.py,sha256=OT9_q2Q1bthzB0g1GlhZVvM4ZAepJRKL6a_Vsr6vEqo,487
64
66
  pytbox/utils/timeutils.py,sha256=uSKgwt20mVcgIGKLsH2tNum8v3rcpzgmBibPvyPQFgM,20433
65
67
  pytbox/win/ad.py,sha256=-3pWfL3dElz-XoO4j4M9lrgu3KJtlhrS9gCWJBpafAU,1147
66
- pytbox-0.2.4.dist-info/METADATA,sha256=29ykO-NI1Kbh7ceqr8--Lzx7_2hz-Cu-croz2RhdHOU,6319
67
- pytbox-0.2.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
68
- pytbox-0.2.4.dist-info/entry_points.txt,sha256=YaTOJ2oPjOiv2SZwY0UC-UA9QS2phRH1oMvxGnxO0Js,43
69
- pytbox-0.2.4.dist-info/top_level.txt,sha256=YADgWue-Oe128ptN3J2hS3GB0Ncc5uZaSUM3e1rwswE,7
70
- pytbox-0.2.4.dist-info/RECORD,,
68
+ pytbox-0.2.6.dist-info/METADATA,sha256=imFjJeLVrjd09kC9dBKWYtVIGXH4hphvPh7mTAQTw9A,6319
69
+ pytbox-0.2.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
70
+ pytbox-0.2.6.dist-info/entry_points.txt,sha256=YaTOJ2oPjOiv2SZwY0UC-UA9QS2phRH1oMvxGnxO0Js,43
71
+ pytbox-0.2.6.dist-info/top_level.txt,sha256=YADgWue-Oe128ptN3J2hS3GB0Ncc5uZaSUM3e1rwswE,7
72
+ pytbox-0.2.6.dist-info/RECORD,,
File without changes