pytbox 0.0.7__py3-none-any.whl → 0.3.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/alert/alert_handler.py +27 -7
- pytbox/alert/ping.py +0 -1
- pytbox/alicloud/sls.py +9 -6
- pytbox/base.py +74 -1
- pytbox/categraf/build_config.py +95 -32
- pytbox/categraf/instances.toml +39 -0
- pytbox/categraf/jinja2/__init__.py +6 -0
- pytbox/categraf/jinja2/input.cpu/cpu.toml.j2 +5 -0
- pytbox/categraf/jinja2/input.disk/disk.toml.j2 +11 -0
- pytbox/categraf/jinja2/input.diskio/diskio.toml.j2 +6 -0
- pytbox/categraf/jinja2/input.dns_query/dns_query.toml.j2 +12 -0
- pytbox/categraf/jinja2/input.http_response/http_response.toml.j2 +9 -0
- pytbox/categraf/jinja2/input.mem/mem.toml.j2 +5 -0
- pytbox/categraf/jinja2/input.net/net.toml.j2 +11 -0
- pytbox/categraf/jinja2/input.net_response/net_response.toml.j2 +9 -0
- pytbox/categraf/jinja2/input.ping/ping.toml.j2 +11 -0
- pytbox/categraf/jinja2/input.prometheus/prometheus.toml.j2 +12 -0
- pytbox/categraf/jinja2/input.snmp/cisco_interface.toml.j2 +96 -0
- pytbox/categraf/jinja2/input.snmp/cisco_system.toml.j2 +41 -0
- pytbox/categraf/jinja2/input.snmp/h3c_interface.toml.j2 +96 -0
- pytbox/categraf/jinja2/input.snmp/h3c_system.toml.j2 +41 -0
- pytbox/categraf/jinja2/input.snmp/huawei_interface.toml.j2 +96 -0
- pytbox/categraf/jinja2/input.snmp/huawei_system.toml.j2 +41 -0
- pytbox/categraf/jinja2/input.snmp/ruijie_interface.toml.j2 +96 -0
- pytbox/categraf/jinja2/input.snmp/ruijie_system.toml.j2 +41 -0
- pytbox/categraf/jinja2/input.vsphere/vsphere.toml.j2 +211 -0
- pytbox/cli/commands/vm.py +22 -0
- pytbox/cli/main.py +2 -0
- pytbox/database/mongo.py +1 -1
- pytbox/database/victoriametrics.py +331 -40
- pytbox/excel.py +64 -0
- pytbox/feishu/endpoints.py +6 -6
- pytbox/log/logger.py +29 -12
- pytbox/mail/alimail.py +142 -0
- pytbox/mail/client.py +171 -0
- pytbox/mail/mail_detail.py +30 -0
- pytbox/mingdao.py +164 -0
- pytbox/network/meraki.py +537 -0
- pytbox/notion.py +731 -0
- pytbox/pyjira.py +612 -0
- pytbox/utils/cronjob.py +79 -0
- pytbox/utils/env.py +2 -2
- pytbox/utils/load_config.py +67 -21
- pytbox/utils/load_vm_devfile.py +45 -0
- pytbox/utils/richutils.py +11 -1
- pytbox/utils/timeutils.py +15 -57
- pytbox/vmware.py +120 -0
- pytbox/win/ad.py +30 -0
- {pytbox-0.0.7.dist-info → pytbox-0.3.1.dist-info}/METADATA +7 -4
- pytbox-0.3.1.dist-info/RECORD +72 -0
- pytbox/utils/ping_checker.py +0 -1
- pytbox-0.0.7.dist-info/RECORD +0 -39
- {pytbox-0.0.7.dist-info → pytbox-0.3.1.dist-info}/WHEEL +0 -0
- {pytbox-0.0.7.dist-info → pytbox-0.3.1.dist-info}/entry_points.txt +0 -0
- {pytbox-0.0.7.dist-info → pytbox-0.3.1.dist-info}/top_level.txt +0 -0
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
|
+
)
|