wecom-doc-sdk 0.1.0__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.
- wecom_doc_sdk/__init__.py +93 -0
- wecom_doc_sdk/apis/__init__.py +5 -0
- wecom_doc_sdk/apis/smartsheet.py +345 -0
- wecom_doc_sdk/client.py +212 -0
- wecom_doc_sdk/exceptions.py +44 -0
- wecom_doc_sdk/models/__init__.py +253 -0
- wecom_doc_sdk/models/common.py +47 -0
- wecom_doc_sdk/models/enums.py +264 -0
- wecom_doc_sdk/models/fields.py +337 -0
- wecom_doc_sdk/models/groups.py +88 -0
- wecom_doc_sdk/models/records.py +231 -0
- wecom_doc_sdk/models/sheets.py +101 -0
- wecom_doc_sdk/models/views.py +243 -0
- wecom_doc_sdk-0.1.0.dist-info/METADATA +236 -0
- wecom_doc_sdk-0.1.0.dist-info/RECORD +16 -0
- wecom_doc_sdk-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""企业微信文档 SDK 入口。"""
|
|
2
|
+
|
|
3
|
+
from .client import AccessTokenProvider, WeComClient
|
|
4
|
+
from .exceptions import WeComAPIError, WeComRequestError
|
|
5
|
+
from .models import (
|
|
6
|
+
AddField,
|
|
7
|
+
AddFieldGroupRequest,
|
|
8
|
+
AddFieldGroupResponse,
|
|
9
|
+
AddFieldsRequest,
|
|
10
|
+
AddFieldsResponse,
|
|
11
|
+
AddRecord,
|
|
12
|
+
AddRecordsRequest,
|
|
13
|
+
AddRecordsResponse,
|
|
14
|
+
AddSheetRequest,
|
|
15
|
+
AddSheetResponse,
|
|
16
|
+
AddViewRequest,
|
|
17
|
+
AddViewResponse,
|
|
18
|
+
CellValueKeyType,
|
|
19
|
+
FieldType,
|
|
20
|
+
GetFieldGroupsRequest,
|
|
21
|
+
GetFieldGroupsResponse,
|
|
22
|
+
GetFieldsRequest,
|
|
23
|
+
GetFieldsResponse,
|
|
24
|
+
GetRecordsRequest,
|
|
25
|
+
GetRecordsResponse,
|
|
26
|
+
GetSheetRequest,
|
|
27
|
+
GetSheetResponse,
|
|
28
|
+
GetViewsRequest,
|
|
29
|
+
GetViewsResponse,
|
|
30
|
+
Record,
|
|
31
|
+
UpdateField,
|
|
32
|
+
UpdateFieldGroupRequest,
|
|
33
|
+
UpdateFieldGroupResponse,
|
|
34
|
+
UpdateFieldsRequest,
|
|
35
|
+
UpdateFieldsResponse,
|
|
36
|
+
UpdateRecord,
|
|
37
|
+
UpdateRecordsRequest,
|
|
38
|
+
UpdateRecordsResponse,
|
|
39
|
+
UpdateSheetRequest,
|
|
40
|
+
UpdateSheetResponse,
|
|
41
|
+
UpdateViewRequest,
|
|
42
|
+
UpdateViewResponse,
|
|
43
|
+
ViewType,
|
|
44
|
+
WeComBaseModel,
|
|
45
|
+
WeComBaseResponse,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
__all__ = [
|
|
49
|
+
"AccessTokenProvider",
|
|
50
|
+
"WeComClient",
|
|
51
|
+
"WeComAPIError",
|
|
52
|
+
"WeComRequestError",
|
|
53
|
+
"WeComBaseModel",
|
|
54
|
+
"WeComBaseResponse",
|
|
55
|
+
"FieldType",
|
|
56
|
+
"ViewType",
|
|
57
|
+
"CellValueKeyType",
|
|
58
|
+
"AddSheetRequest",
|
|
59
|
+
"AddSheetResponse",
|
|
60
|
+
"UpdateSheetRequest",
|
|
61
|
+
"UpdateSheetResponse",
|
|
62
|
+
"GetSheetRequest",
|
|
63
|
+
"GetSheetResponse",
|
|
64
|
+
"AddViewRequest",
|
|
65
|
+
"AddViewResponse",
|
|
66
|
+
"UpdateViewRequest",
|
|
67
|
+
"UpdateViewResponse",
|
|
68
|
+
"GetViewsRequest",
|
|
69
|
+
"GetViewsResponse",
|
|
70
|
+
"AddField",
|
|
71
|
+
"AddFieldsRequest",
|
|
72
|
+
"AddFieldsResponse",
|
|
73
|
+
"UpdateField",
|
|
74
|
+
"UpdateFieldsRequest",
|
|
75
|
+
"UpdateFieldsResponse",
|
|
76
|
+
"GetFieldsRequest",
|
|
77
|
+
"GetFieldsResponse",
|
|
78
|
+
"AddRecord",
|
|
79
|
+
"AddRecordsRequest",
|
|
80
|
+
"AddRecordsResponse",
|
|
81
|
+
"UpdateRecord",
|
|
82
|
+
"UpdateRecordsRequest",
|
|
83
|
+
"UpdateRecordsResponse",
|
|
84
|
+
"GetRecordsRequest",
|
|
85
|
+
"GetRecordsResponse",
|
|
86
|
+
"Record",
|
|
87
|
+
"AddFieldGroupRequest",
|
|
88
|
+
"AddFieldGroupResponse",
|
|
89
|
+
"UpdateFieldGroupRequest",
|
|
90
|
+
"UpdateFieldGroupResponse",
|
|
91
|
+
"GetFieldGroupsRequest",
|
|
92
|
+
"GetFieldGroupsResponse",
|
|
93
|
+
]
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Type, TypeVar
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
from ..client import WeComClient
|
|
8
|
+
from ..models.fields import (
|
|
9
|
+
AddFieldsRequest,
|
|
10
|
+
AddFieldsResponse,
|
|
11
|
+
DeleteFieldsRequest,
|
|
12
|
+
DeleteFieldsResponse,
|
|
13
|
+
GetFieldsRequest,
|
|
14
|
+
GetFieldsResponse,
|
|
15
|
+
UpdateFieldsRequest,
|
|
16
|
+
UpdateFieldsResponse,
|
|
17
|
+
)
|
|
18
|
+
from ..models.groups import (
|
|
19
|
+
AddFieldGroupRequest,
|
|
20
|
+
AddFieldGroupResponse,
|
|
21
|
+
DeleteFieldGroupsRequest,
|
|
22
|
+
DeleteFieldGroupsResponse,
|
|
23
|
+
GetFieldGroupsRequest,
|
|
24
|
+
GetFieldGroupsResponse,
|
|
25
|
+
UpdateFieldGroupRequest,
|
|
26
|
+
UpdateFieldGroupResponse,
|
|
27
|
+
)
|
|
28
|
+
from ..models.records import (
|
|
29
|
+
AddRecordsRequest,
|
|
30
|
+
AddRecordsResponse,
|
|
31
|
+
DeleteRecordsRequest,
|
|
32
|
+
DeleteRecordsResponse,
|
|
33
|
+
GetRecordsRequest,
|
|
34
|
+
GetRecordsResponse,
|
|
35
|
+
UpdateRecordsRequest,
|
|
36
|
+
UpdateRecordsResponse,
|
|
37
|
+
)
|
|
38
|
+
from ..models.sheets import (
|
|
39
|
+
AddSheetRequest,
|
|
40
|
+
AddSheetResponse,
|
|
41
|
+
DeleteSheetRequest,
|
|
42
|
+
DeleteSheetResponse,
|
|
43
|
+
GetSheetRequest,
|
|
44
|
+
GetSheetResponse,
|
|
45
|
+
UpdateSheetRequest,
|
|
46
|
+
UpdateSheetResponse,
|
|
47
|
+
)
|
|
48
|
+
from ..models.views import (
|
|
49
|
+
AddViewRequest,
|
|
50
|
+
AddViewResponse,
|
|
51
|
+
DeleteViewsRequest,
|
|
52
|
+
DeleteViewsResponse,
|
|
53
|
+
GetViewsRequest,
|
|
54
|
+
GetViewsResponse,
|
|
55
|
+
UpdateViewRequest,
|
|
56
|
+
UpdateViewResponse,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
TModel = TypeVar("TModel", bound=BaseModel)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class SmartSheetAPI:
|
|
63
|
+
"""管理智能表格内容相关接口封装。"""
|
|
64
|
+
|
|
65
|
+
def __init__(self, client: WeComClient) -> None:
|
|
66
|
+
self._client = client
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def _ensure_model(model_cls: Type[TModel], payload: Any) -> TModel:
|
|
70
|
+
if isinstance(payload, model_cls):
|
|
71
|
+
return payload
|
|
72
|
+
return model_cls.model_validate(payload)
|
|
73
|
+
|
|
74
|
+
# --- 子表 ---
|
|
75
|
+
def add_sheet(self, request: AddSheetRequest | dict[str, Any]) -> AddSheetResponse:
|
|
76
|
+
"""在文档的指定位置新增一个智能表子表。
|
|
77
|
+
|
|
78
|
+
新建子表初始不包含视图、记录和字段,后续需再调用对应接口补充内容。
|
|
79
|
+
"""
|
|
80
|
+
req = self._ensure_model(AddSheetRequest, request)
|
|
81
|
+
data = self._client.request_json(
|
|
82
|
+
"POST",
|
|
83
|
+
"/cgi-bin/wedoc/smartsheet/add_sheet",
|
|
84
|
+
json=self._client.dump_model(req),
|
|
85
|
+
)
|
|
86
|
+
return AddSheetResponse.model_validate(data)
|
|
87
|
+
|
|
88
|
+
def delete_sheet(
|
|
89
|
+
self, request: DeleteSheetRequest | dict[str, Any]
|
|
90
|
+
) -> DeleteSheetResponse:
|
|
91
|
+
"""删除在线表格中的指定子表。"""
|
|
92
|
+
req = self._ensure_model(DeleteSheetRequest, request)
|
|
93
|
+
data = self._client.request_json(
|
|
94
|
+
"POST",
|
|
95
|
+
"/cgi-bin/wedoc/smartsheet/delete_sheet",
|
|
96
|
+
json=self._client.dump_model(req),
|
|
97
|
+
)
|
|
98
|
+
return DeleteSheetResponse.model_validate(data)
|
|
99
|
+
|
|
100
|
+
def update_sheet(
|
|
101
|
+
self, request: UpdateSheetRequest | dict[str, Any]
|
|
102
|
+
) -> UpdateSheetResponse:
|
|
103
|
+
"""更新子表信息,当前主要用于修改子表标题。"""
|
|
104
|
+
req = self._ensure_model(UpdateSheetRequest, request)
|
|
105
|
+
data = self._client.request_json(
|
|
106
|
+
"POST",
|
|
107
|
+
"/cgi-bin/wedoc/smartsheet/update_sheet",
|
|
108
|
+
json=self._client.dump_model(req),
|
|
109
|
+
)
|
|
110
|
+
return UpdateSheetResponse.model_validate(data)
|
|
111
|
+
|
|
112
|
+
def get_sheet(self, request: GetSheetRequest | dict[str, Any]) -> GetSheetResponse:
|
|
113
|
+
"""查询文档下的子表信息。"""
|
|
114
|
+
req = self._ensure_model(GetSheetRequest, request)
|
|
115
|
+
data = self._client.request_json(
|
|
116
|
+
"POST",
|
|
117
|
+
"/cgi-bin/wedoc/smartsheet/get_sheet",
|
|
118
|
+
json=self._client.dump_model(req),
|
|
119
|
+
)
|
|
120
|
+
return GetSheetResponse.model_validate(data)
|
|
121
|
+
|
|
122
|
+
# --- 视图 ---
|
|
123
|
+
def add_view(self, request: AddViewRequest | dict[str, Any]) -> AddViewResponse:
|
|
124
|
+
"""在子表中新增视图。
|
|
125
|
+
|
|
126
|
+
单表最多允许 200 个视图;添加甘特图或日历视图时需传入对应属性。
|
|
127
|
+
"""
|
|
128
|
+
req = self._ensure_model(AddViewRequest, request)
|
|
129
|
+
data = self._client.request_json(
|
|
130
|
+
"POST",
|
|
131
|
+
"/cgi-bin/wedoc/smartsheet/add_view",
|
|
132
|
+
json=self._client.dump_model(req),
|
|
133
|
+
)
|
|
134
|
+
return AddViewResponse.model_validate(data)
|
|
135
|
+
|
|
136
|
+
def delete_views(
|
|
137
|
+
self, request: DeleteViewsRequest | dict[str, Any]
|
|
138
|
+
) -> DeleteViewsResponse:
|
|
139
|
+
"""批量删除子表中的一个或多个视图。"""
|
|
140
|
+
req = self._ensure_model(DeleteViewsRequest, request)
|
|
141
|
+
data = self._client.request_json(
|
|
142
|
+
"POST",
|
|
143
|
+
"/cgi-bin/wedoc/smartsheet/delete_views",
|
|
144
|
+
json=self._client.dump_model(req),
|
|
145
|
+
)
|
|
146
|
+
return DeleteViewsResponse.model_validate(data)
|
|
147
|
+
|
|
148
|
+
def update_view(
|
|
149
|
+
self, request: UpdateViewRequest | dict[str, Any]
|
|
150
|
+
) -> UpdateViewResponse:
|
|
151
|
+
"""更新视图标题或视图属性。
|
|
152
|
+
|
|
153
|
+
支持调整排序、过滤、分组、字段显示、冻结列和填色等配置。
|
|
154
|
+
"""
|
|
155
|
+
req = self._ensure_model(UpdateViewRequest, request)
|
|
156
|
+
data = self._client.request_json(
|
|
157
|
+
"POST",
|
|
158
|
+
"/cgi-bin/wedoc/smartsheet/update_view",
|
|
159
|
+
json=self._client.dump_model(req),
|
|
160
|
+
)
|
|
161
|
+
return UpdateViewResponse.model_validate(data)
|
|
162
|
+
|
|
163
|
+
def get_views(self, request: GetViewsRequest | dict[str, Any]) -> GetViewsResponse:
|
|
164
|
+
"""获取子表下全部视图信息。"""
|
|
165
|
+
req = self._ensure_model(GetViewsRequest, request)
|
|
166
|
+
data = self._client.request_json(
|
|
167
|
+
"POST",
|
|
168
|
+
"/cgi-bin/wedoc/smartsheet/get_views",
|
|
169
|
+
json=self._client.dump_model(req),
|
|
170
|
+
)
|
|
171
|
+
return GetViewsResponse.model_validate(data)
|
|
172
|
+
|
|
173
|
+
# --- 字段 ---
|
|
174
|
+
def add_fields(
|
|
175
|
+
self, request: AddFieldsRequest | dict[str, Any]
|
|
176
|
+
) -> AddFieldsResponse:
|
|
177
|
+
"""在子表中新增一个或多个字段。
|
|
178
|
+
|
|
179
|
+
单表最多允许 150 个字段,字段类型与字段属性必须严格匹配。
|
|
180
|
+
"""
|
|
181
|
+
req = self._ensure_model(AddFieldsRequest, request)
|
|
182
|
+
data = self._client.request_json(
|
|
183
|
+
"POST",
|
|
184
|
+
"/cgi-bin/wedoc/smartsheet/add_fields",
|
|
185
|
+
json=self._client.dump_model(req),
|
|
186
|
+
)
|
|
187
|
+
return AddFieldsResponse.model_validate(data)
|
|
188
|
+
|
|
189
|
+
def delete_fields(
|
|
190
|
+
self, request: DeleteFieldsRequest | dict[str, Any]
|
|
191
|
+
) -> DeleteFieldsResponse:
|
|
192
|
+
"""批量删除子表中的字段。"""
|
|
193
|
+
req = self._ensure_model(DeleteFieldsRequest, request)
|
|
194
|
+
data = self._client.request_json(
|
|
195
|
+
"POST",
|
|
196
|
+
"/cgi-bin/wedoc/smartsheet/delete_fields",
|
|
197
|
+
json=self._client.dump_model(req),
|
|
198
|
+
)
|
|
199
|
+
return DeleteFieldsResponse.model_validate(data)
|
|
200
|
+
|
|
201
|
+
def update_fields(
|
|
202
|
+
self, request: UpdateFieldsRequest | dict[str, Any]
|
|
203
|
+
) -> UpdateFieldsResponse:
|
|
204
|
+
"""更新字段标题或字段属性。
|
|
205
|
+
|
|
206
|
+
该接口不支持修改字段类型;更新时至少应提供待变更的标题或字段属性。
|
|
207
|
+
"""
|
|
208
|
+
req = self._ensure_model(UpdateFieldsRequest, request)
|
|
209
|
+
data = self._client.request_json(
|
|
210
|
+
"POST",
|
|
211
|
+
"/cgi-bin/wedoc/smartsheet/update_fields",
|
|
212
|
+
json=self._client.dump_model(req),
|
|
213
|
+
)
|
|
214
|
+
return UpdateFieldsResponse.model_validate(data)
|
|
215
|
+
|
|
216
|
+
def get_fields(
|
|
217
|
+
self, request: GetFieldsRequest | dict[str, Any]
|
|
218
|
+
) -> GetFieldsResponse:
|
|
219
|
+
"""查询子表字段信息。
|
|
220
|
+
|
|
221
|
+
支持按字段 ID、字段标题或分页方式获取,单次 `limit` 最大为 1000。
|
|
222
|
+
"""
|
|
223
|
+
req = self._ensure_model(GetFieldsRequest, request)
|
|
224
|
+
data = self._client.request_json(
|
|
225
|
+
"POST",
|
|
226
|
+
"/cgi-bin/wedoc/smartsheet/get_fields",
|
|
227
|
+
json=self._client.dump_model(req),
|
|
228
|
+
)
|
|
229
|
+
return GetFieldsResponse.model_validate(data)
|
|
230
|
+
|
|
231
|
+
# --- 记录 ---
|
|
232
|
+
def add_records(
|
|
233
|
+
self, request: AddRecordsRequest | dict[str, Any]
|
|
234
|
+
) -> AddRecordsResponse:
|
|
235
|
+
"""在子表中新增一行或多行记录。
|
|
236
|
+
|
|
237
|
+
单次添加建议控制在 500 行内,且不能写入创建时间、最后编辑时间、创建人、
|
|
238
|
+
最后编辑人字段。
|
|
239
|
+
"""
|
|
240
|
+
req = self._ensure_model(AddRecordsRequest, request)
|
|
241
|
+
data = self._client.request_json(
|
|
242
|
+
"POST",
|
|
243
|
+
"/cgi-bin/wedoc/smartsheet/add_records",
|
|
244
|
+
json=self._client.dump_model(req),
|
|
245
|
+
)
|
|
246
|
+
return AddRecordsResponse.model_validate(data)
|
|
247
|
+
|
|
248
|
+
def delete_records(
|
|
249
|
+
self, request: DeleteRecordsRequest | dict[str, Any]
|
|
250
|
+
) -> DeleteRecordsResponse:
|
|
251
|
+
"""批量删除子表中的记录,单次删除建议控制在 500 行内。"""
|
|
252
|
+
req = self._ensure_model(DeleteRecordsRequest, request)
|
|
253
|
+
data = self._client.request_json(
|
|
254
|
+
"POST",
|
|
255
|
+
"/cgi-bin/wedoc/smartsheet/delete_records",
|
|
256
|
+
json=self._client.dump_model(req),
|
|
257
|
+
)
|
|
258
|
+
return DeleteRecordsResponse.model_validate(data)
|
|
259
|
+
|
|
260
|
+
def update_records(
|
|
261
|
+
self, request: UpdateRecordsRequest | dict[str, Any]
|
|
262
|
+
) -> UpdateRecordsResponse:
|
|
263
|
+
"""更新子表中的一行或多行记录。
|
|
264
|
+
|
|
265
|
+
单次更新建议控制在 500 行内,且不能更新创建时间、最后编辑时间、创建人、
|
|
266
|
+
最后编辑人字段。
|
|
267
|
+
"""
|
|
268
|
+
req = self._ensure_model(UpdateRecordsRequest, request)
|
|
269
|
+
data = self._client.request_json(
|
|
270
|
+
"POST",
|
|
271
|
+
"/cgi-bin/wedoc/smartsheet/update_records",
|
|
272
|
+
json=self._client.dump_model(req),
|
|
273
|
+
)
|
|
274
|
+
return UpdateRecordsResponse.model_validate(data)
|
|
275
|
+
|
|
276
|
+
def get_records(
|
|
277
|
+
self, request: GetRecordsRequest | dict[str, Any]
|
|
278
|
+
) -> GetRecordsResponse:
|
|
279
|
+
"""查询子表记录信息。
|
|
280
|
+
|
|
281
|
+
支持全量查询、按记录或字段筛选以及排序;`filter_spec` 与 `sort` 不能同时使用,
|
|
282
|
+
单次 `limit` 最大为 1000。
|
|
283
|
+
"""
|
|
284
|
+
req = self._ensure_model(GetRecordsRequest, request)
|
|
285
|
+
data = self._client.request_json(
|
|
286
|
+
"POST",
|
|
287
|
+
"/cgi-bin/wedoc/smartsheet/get_records",
|
|
288
|
+
json=self._client.dump_model(req),
|
|
289
|
+
)
|
|
290
|
+
return GetRecordsResponse.model_validate(data)
|
|
291
|
+
|
|
292
|
+
# --- 编组 ---
|
|
293
|
+
def add_field_group(
|
|
294
|
+
self, request: AddFieldGroupRequest | dict[str, Any]
|
|
295
|
+
) -> AddFieldGroupResponse:
|
|
296
|
+
"""在子表中新增编组。
|
|
297
|
+
|
|
298
|
+
单表最多允许 150 个编组,每个编组最多 150 个字段,且字段只能同时属于一个编组。
|
|
299
|
+
"""
|
|
300
|
+
req = self._ensure_model(AddFieldGroupRequest, request)
|
|
301
|
+
data = self._client.request_json(
|
|
302
|
+
"POST",
|
|
303
|
+
"/cgi-bin/wedoc/smartsheet/add_field_group",
|
|
304
|
+
json=self._client.dump_model(req),
|
|
305
|
+
)
|
|
306
|
+
return AddFieldGroupResponse.model_validate(data)
|
|
307
|
+
|
|
308
|
+
def update_field_group(
|
|
309
|
+
self, request: UpdateFieldGroupRequest | dict[str, Any]
|
|
310
|
+
) -> UpdateFieldGroupResponse:
|
|
311
|
+
"""更新已有编组的名称或字段成员。
|
|
312
|
+
|
|
313
|
+
编组名称不能与已有编组重复,字段仍只能同时属于一个编组。
|
|
314
|
+
"""
|
|
315
|
+
req = self._ensure_model(UpdateFieldGroupRequest, request)
|
|
316
|
+
data = self._client.request_json(
|
|
317
|
+
"POST",
|
|
318
|
+
"/cgi-bin/wedoc/smartsheet/update_field_group",
|
|
319
|
+
json=self._client.dump_model(req),
|
|
320
|
+
)
|
|
321
|
+
return UpdateFieldGroupResponse.model_validate(data)
|
|
322
|
+
|
|
323
|
+
def delete_field_groups(
|
|
324
|
+
self, request: DeleteFieldGroupsRequest | dict[str, Any]
|
|
325
|
+
) -> DeleteFieldGroupsResponse:
|
|
326
|
+
"""批量删除子表中的一个或多个编组。"""
|
|
327
|
+
req = self._ensure_model(DeleteFieldGroupsRequest, request)
|
|
328
|
+
data = self._client.request_json(
|
|
329
|
+
"POST",
|
|
330
|
+
"/cgi-bin/wedoc/smartsheet/delete_field_groups",
|
|
331
|
+
json=self._client.dump_model(req),
|
|
332
|
+
)
|
|
333
|
+
return DeleteFieldGroupsResponse.model_validate(data)
|
|
334
|
+
|
|
335
|
+
def get_field_groups(
|
|
336
|
+
self, request: GetFieldGroupsRequest | dict[str, Any]
|
|
337
|
+
) -> GetFieldGroupsResponse:
|
|
338
|
+
"""获取子表下已有的编组信息。"""
|
|
339
|
+
req = self._ensure_model(GetFieldGroupsRequest, request)
|
|
340
|
+
data = self._client.request_json(
|
|
341
|
+
"POST",
|
|
342
|
+
"/cgi-bin/wedoc/smartsheet/get_field_groups",
|
|
343
|
+
json=self._client.dump_model(req),
|
|
344
|
+
)
|
|
345
|
+
return GetFieldGroupsResponse.model_validate(data)
|
wecom_doc_sdk/client.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from .exceptions import WeComAPIError, WeComRequestError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AccessTokenResponse(BaseModel):
|
|
14
|
+
"""`/cgi-bin/gettoken` 接口响应。
|
|
15
|
+
|
|
16
|
+
企业微信会返回凭证本身及其秒级有效期。SDK 会基于该结构做本地缓存,
|
|
17
|
+
并在凭证过期前提前刷新,避免业务请求在边界时刻拿到失效 token。
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
errcode: int = 0
|
|
21
|
+
errmsg: str = ""
|
|
22
|
+
access_token: Optional[str] = None
|
|
23
|
+
expires_in: Optional[int] = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AccessTokenProvider:
|
|
27
|
+
"""access_token 获取与缓存。
|
|
28
|
+
|
|
29
|
+
说明:
|
|
30
|
+
- 内部会做简单缓存与提前刷新(默认提前 120 秒)。
|
|
31
|
+
- 仅用于企业微信自建应用的 corpid/secret 换取 token 场景。
|
|
32
|
+
- 企业微信官方文档要求开发者在服务端缓存 token,并在失效时重新获取。
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
corp_id: str,
|
|
38
|
+
corp_secret: str,
|
|
39
|
+
*,
|
|
40
|
+
base_url: str = "https://qyapi.weixin.qq.com",
|
|
41
|
+
timeout: float = 10.0,
|
|
42
|
+
refresh_buffer: int = 120,
|
|
43
|
+
http_client: Optional[httpx.Client] = None,
|
|
44
|
+
) -> None:
|
|
45
|
+
self._corp_id = corp_id
|
|
46
|
+
self._corp_secret = corp_secret
|
|
47
|
+
self._refresh_buffer = refresh_buffer
|
|
48
|
+
self._lock = threading.Lock()
|
|
49
|
+
self._token: Optional[str] = None
|
|
50
|
+
self._expire_at: float = 0.0
|
|
51
|
+
self._owns_client = http_client is None
|
|
52
|
+
self._client = http_client or httpx.Client(base_url=base_url, timeout=timeout)
|
|
53
|
+
|
|
54
|
+
def close(self) -> None:
|
|
55
|
+
"""关闭内部持有的 HTTP 客户端。
|
|
56
|
+
|
|
57
|
+
当调用方复用了外部传入的 `httpx.Client` 时,由调用方自己负责关闭。
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
if self._owns_client:
|
|
61
|
+
self._client.close()
|
|
62
|
+
|
|
63
|
+
def get(self) -> str:
|
|
64
|
+
"""获取当前可用的 access_token。
|
|
65
|
+
|
|
66
|
+
优先返回缓存中的 token;如果缓存不存在或即将过期,则在锁保护下刷新。
|
|
67
|
+
这样可以避免多线程环境下重复请求 `/cgi-bin/gettoken`。
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
if self._token and time.time() < self._expire_at - self._refresh_buffer:
|
|
71
|
+
return self._token
|
|
72
|
+
# 多线程场景下仅允许一个线程刷新
|
|
73
|
+
with self._lock:
|
|
74
|
+
if self._token and time.time() < self._expire_at - self._refresh_buffer:
|
|
75
|
+
return self._token
|
|
76
|
+
self._refresh_token()
|
|
77
|
+
if not self._token:
|
|
78
|
+
raise WeComRequestError("获取 access_token 失败:返回为空")
|
|
79
|
+
return self._token
|
|
80
|
+
|
|
81
|
+
def _refresh_token(self) -> None:
|
|
82
|
+
"""调用企业微信 `gettoken` 接口刷新缓存中的 access_token。"""
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
resp = self._client.get(
|
|
86
|
+
"/cgi-bin/gettoken",
|
|
87
|
+
params={"corpid": self._corp_id, "corpsecret": self._corp_secret},
|
|
88
|
+
)
|
|
89
|
+
except httpx.HTTPError as exc:
|
|
90
|
+
raise WeComRequestError(
|
|
91
|
+
"获取 access_token 失败:网络异常", cause=exc
|
|
92
|
+
) from exc
|
|
93
|
+
|
|
94
|
+
if resp.status_code >= 400:
|
|
95
|
+
raise WeComRequestError(
|
|
96
|
+
"获取 access_token 失败:HTTP 状态异常", response=resp
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
payload = resp.json()
|
|
101
|
+
except ValueError as exc:
|
|
102
|
+
raise WeComRequestError(
|
|
103
|
+
"获取 access_token 失败:响应不是 JSON", response=resp, cause=exc
|
|
104
|
+
) from exc
|
|
105
|
+
|
|
106
|
+
data = AccessTokenResponse.model_validate(payload)
|
|
107
|
+
# 企业微信官方文档明确要求以 errcode 判断是否成功,errmsg 仅用于辅助排障。
|
|
108
|
+
if data.errcode != 0:
|
|
109
|
+
raise WeComAPIError(data.errcode, data.errmsg, payload)
|
|
110
|
+
|
|
111
|
+
self._token = data.access_token
|
|
112
|
+
expires_in = int(data.expires_in or 0)
|
|
113
|
+
# 记录过期时间(秒级);后续读取时会结合 refresh_buffer 提前刷新。
|
|
114
|
+
self._expire_at = time.time() + expires_in
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class WeComClient:
|
|
118
|
+
"""企业微信文档 SDK 客户端(同步版)。
|
|
119
|
+
|
|
120
|
+
负责统一管理底层 HTTP 连接、access_token 获取逻辑,以及各业务 API 模块。
|
|
121
|
+
当前默认挂载智能表格内容相关接口:`smartsheet`。
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
def __init__(
|
|
125
|
+
self,
|
|
126
|
+
corp_id: str,
|
|
127
|
+
corp_secret: str,
|
|
128
|
+
*,
|
|
129
|
+
base_url: str = "https://qyapi.weixin.qq.com",
|
|
130
|
+
timeout: float = 10.0,
|
|
131
|
+
) -> None:
|
|
132
|
+
self._http = httpx.Client(base_url=base_url, timeout=timeout)
|
|
133
|
+
# 复用 httpx.Client 以减少连接开销,并与 token 获取共用一套超时配置。
|
|
134
|
+
self._token_provider = AccessTokenProvider(
|
|
135
|
+
corp_id,
|
|
136
|
+
corp_secret,
|
|
137
|
+
base_url=base_url,
|
|
138
|
+
timeout=timeout,
|
|
139
|
+
http_client=self._http,
|
|
140
|
+
)
|
|
141
|
+
# 延迟导入避免循环依赖
|
|
142
|
+
from .apis.smartsheet import SmartSheetAPI
|
|
143
|
+
|
|
144
|
+
self.smartsheet = SmartSheetAPI(self)
|
|
145
|
+
|
|
146
|
+
def close(self) -> None:
|
|
147
|
+
"""关闭底层 HTTP 连接。"""
|
|
148
|
+
|
|
149
|
+
self._http.close()
|
|
150
|
+
|
|
151
|
+
def __enter__(self) -> "WeComClient":
|
|
152
|
+
return self
|
|
153
|
+
|
|
154
|
+
def __exit__(self, exc_type, exc, tb) -> None: # noqa: D401 - 遵循上下文管理协议
|
|
155
|
+
self.close()
|
|
156
|
+
|
|
157
|
+
def request_json(
|
|
158
|
+
self,
|
|
159
|
+
method: str,
|
|
160
|
+
path: str,
|
|
161
|
+
*,
|
|
162
|
+
params: Optional[Dict[str, Any]] = None,
|
|
163
|
+
json: Optional[Dict[str, Any]] = None,
|
|
164
|
+
) -> Dict[str, Any]:
|
|
165
|
+
"""统一发送企业微信 JSON 请求。
|
|
166
|
+
|
|
167
|
+
该方法会自动注入 `access_token`,并统一处理三类错误:
|
|
168
|
+
- 网络层或 HTTP 状态异常;
|
|
169
|
+
- 响应体不是合法 JSON;
|
|
170
|
+
- 企业微信返回 `errcode != 0` 的业务错误。
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
token = self._token_provider.get()
|
|
174
|
+
request_params = dict(params or {})
|
|
175
|
+
# 企业微信大多数服务端接口都要求将 access_token 放在查询参数中。
|
|
176
|
+
request_params["access_token"] = token
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
response = self._http.request(
|
|
180
|
+
method, path, params=request_params, json=json
|
|
181
|
+
)
|
|
182
|
+
except httpx.HTTPError as exc:
|
|
183
|
+
raise WeComRequestError("请求企业微信接口失败", cause=exc) from exc
|
|
184
|
+
|
|
185
|
+
if response.status_code >= 400:
|
|
186
|
+
raise WeComRequestError(
|
|
187
|
+
"请求企业微信接口失败:HTTP 状态异常", response=response
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
payload = response.json()
|
|
192
|
+
except ValueError as exc:
|
|
193
|
+
raise WeComRequestError(
|
|
194
|
+
"响应解析失败:不是 JSON", response=response, cause=exc
|
|
195
|
+
) from exc
|
|
196
|
+
|
|
197
|
+
# 根据企业微信全局错误码文档,必须优先使用 errcode 判断业务是否成功。
|
|
198
|
+
if isinstance(payload, dict) and payload.get("errcode", 0) != 0:
|
|
199
|
+
raise WeComAPIError(
|
|
200
|
+
int(payload.get("errcode", -1)), str(payload.get("errmsg", "")), payload
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
return payload
|
|
204
|
+
|
|
205
|
+
@staticmethod
|
|
206
|
+
def dump_model(model: BaseModel) -> Dict[str, Any]:
|
|
207
|
+
"""统一序列化 Pydantic 模型。
|
|
208
|
+
|
|
209
|
+
默认使用字段别名并忽略空值,避免把未填写的可选字段透传给企业微信接口。
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
return model.model_dump(by_alias=True, exclude_none=True)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class WeComAPIError(RuntimeError):
|
|
9
|
+
"""企业微信接口返回业务错误时抛出。
|
|
10
|
+
|
|
11
|
+
这类异常表示请求已成功到达企业微信,但接口返回了非零 `errcode`。
|
|
12
|
+
排查时应优先依据 `errcode`,`errmsg` 只作为辅助说明。
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self, errcode: int, errmsg: str, raw: Optional[dict[str, Any]] = None
|
|
17
|
+
) -> None:
|
|
18
|
+
self.errcode = errcode
|
|
19
|
+
self.errmsg = errmsg
|
|
20
|
+
# 保留企业微信原始 JSON,便于调用方在日志中定位字段级问题。
|
|
21
|
+
self.raw = raw or {}
|
|
22
|
+
super().__init__(f"WeCom API error: {errcode} - {errmsg}")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class WeComRequestError(RuntimeError):
|
|
26
|
+
"""网络请求、HTTP 状态或响应解析失败时抛出。
|
|
27
|
+
|
|
28
|
+
这类异常通常发生在企业微信真正返回业务结果之前,适合用来区分:
|
|
29
|
+
- 网络连接异常;
|
|
30
|
+
- HTTP 状态码异常;
|
|
31
|
+
- 返回体不是预期 JSON。
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
message: str,
|
|
37
|
+
response: Optional[httpx.Response] = None,
|
|
38
|
+
cause: Optional[BaseException] = None,
|
|
39
|
+
) -> None:
|
|
40
|
+
# 原始响应可帮助排查状态码、响应头或非 JSON 返回体。
|
|
41
|
+
self.response = response
|
|
42
|
+
# 透传底层异常,方便日志或调用方保留完整调用栈。
|
|
43
|
+
self.cause = cause
|
|
44
|
+
super().__init__(message)
|