py-wecom-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.
@@ -0,0 +1,6 @@
1
+ from __future__ import annotations
2
+ from .provider import APIProvider as WeComAPIProvider
3
+
4
+ __all__ = [
5
+ "WeComAPIProvider",
6
+ ]
@@ -0,0 +1,17 @@
1
+ """群机器人公共接口,提供同步/异步客户端及响应模型。
2
+
3
+ 官方文档:https://developer.work.weixin.qq.com/document/path/91770
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from ._api import API as GroupRobotAPI, AsyncAPI as GroupRobotAsyncAPI
9
+ from ._models import SendResponse as GroupRobotSendResponse, UploadMediaResponse as GroupRobotUploadMediaResponse
10
+
11
+ # 公共导出名称;官方文档:https://developer.work.weixin.qq.com/document/path/91770
12
+ __all__ = [
13
+ "GroupRobotAPI",
14
+ "GroupRobotAsyncAPI",
15
+ "GroupRobotSendResponse",
16
+ "GroupRobotUploadMediaResponse",
17
+ ]
@@ -0,0 +1,488 @@
1
+ """群机器人同步与异步接口。
2
+
3
+ 官方文档:https://developer.work.weixin.qq.com/document/path/91770
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import warnings
9
+ from pathlib import Path
10
+ from typing import Any, BinaryIO, Literal
11
+
12
+ import httpx
13
+
14
+ from ._models import SendResponse as GroupRobotSendResponse
15
+ from ._models import UploadMediaResponse as GroupRobotUploadMediaResponse
16
+
17
+ __all__ = ["API", "AsyncAPI"]
18
+
19
+ # 企业微信 API 根地址;官方文档:https://developer.work.weixin.qq.com/document/path/91770
20
+ DEFAULT_BASE_URL = "https://qyapi.weixin.qq.com/"
21
+ # 消息发送地址;官方文档:https://developer.work.weixin.qq.com/document/path/91770#消息类型及数据格式
22
+ DEFAULT_SEND_URL = "/cgi-bin/webhook/send"
23
+ # 素材上传地址;官方文档:https://developer.work.weixin.qq.com/document/path/91770#文件上传接口
24
+ DEFAULT_UPLOAD_MEDIA_URL = "/cgi-bin/webhook/upload_media"
25
+
26
+
27
+ def _extract_media(
28
+ media: bytes | bytearray | str | Path | BinaryIO,
29
+ *,
30
+ filename: str | None = None,
31
+ ) -> tuple[str, bytes]:
32
+ """将素材转换为 multipart 上传所需的文件名和二进制内容。
33
+
34
+ Args:
35
+ media: 原始字节、文件路径或已打开的二进制文件对象。
36
+ filename: 显式文件名;未指定时从路径或文件对象推断,最后使用 media。
37
+
38
+ Returns:
39
+ 文件名与原始字节。文件对象从当前位置读取,不负责关闭或重置游标。
40
+
41
+ Raises:
42
+ OSError: 路径不存在、指向目录或无法读取。
43
+ TypeError: 文件对象未返回二进制内容。
44
+
45
+ 官方文档:https://developer.work.weixin.qq.com/document/path/91770#文件上传接口
46
+ """
47
+ if isinstance(media, (bytes, bytearray)):
48
+ return filename or "media", bytes(media)
49
+
50
+ if isinstance(media, (str, Path)):
51
+ path = Path(media)
52
+ return filename or path.name, path.read_bytes()
53
+
54
+ name = getattr(media, "name", None)
55
+ filename = filename or (Path(name).name if isinstance(name, (str, Path)) else "media")
56
+ content = media.read()
57
+ if not isinstance(content, (bytes, bytearray)):
58
+ raise TypeError("media 必须是二进制文件对象,请使用 rb 模式打开文件")
59
+ return filename or "media", bytes(content)
60
+
61
+
62
+ class _BaseAPI:
63
+ """群机器人 API 公共基类,封装初始化和资源管理。"""
64
+
65
+ def __init__(
66
+ self,
67
+ key: str | None,
68
+ mentioned_list: list[str] | None,
69
+ mentioned_mobile_list: list[str] | None,
70
+ base_url: str | None,
71
+ timeout: float | tuple[float, float, float, float],
72
+ client_kwargs: dict[str, Any] | None,
73
+ ):
74
+ self.key = key or ""
75
+ if not self.key:
76
+ warnings.warn("未提供群机器人 key,消息发送将会失败", stacklevel=3)
77
+ self.mentioned_list = mentioned_list or []
78
+ self.mentioned_mobile_list = mentioned_mobile_list or []
79
+ self.base_url = (base_url or DEFAULT_BASE_URL).rstrip("/")
80
+ self.client_kwargs = client_kwargs or {}
81
+ self.client_kwargs.setdefault("base_url", self.base_url)
82
+ self.client_kwargs.setdefault("timeout", timeout)
83
+
84
+
85
+ class API(_BaseAPI):
86
+ """企业微信群机器人同步 API。
87
+
88
+ 官方文档:https://developer.work.weixin.qq.com/document/path/91770
89
+ """
90
+
91
+ def __init__(
92
+ self,
93
+ key: str | None = None,
94
+ mentioned_list: list[str] | None = None,
95
+ mentioned_mobile_list: list[str] | None = None,
96
+ base_url: str | None = None,
97
+ timeout: float | tuple[float, float, float, float] = 60.0,
98
+ client: httpx.Client | None = None,
99
+ client_kwargs: dict[str, Any] | None = None,
100
+ ):
101
+ """初始化同步客户端。
102
+
103
+ Args:
104
+ key: 群机器人的 Webhook key。
105
+ mentioned_list: 默认 @ 的成员 userid 列表。
106
+ mentioned_mobile_list: 默认 @ 的成员手机号列表。
107
+ base_url: API 根地址。
108
+ timeout: HTTP 请求超时时间。
109
+ client: 可选的已配置 httpx.Client。
110
+ client_kwargs: 未传入 client 时创建客户端的配置。
111
+
112
+ 官方文档:https://developer.work.weixin.qq.com/document/path/91770
113
+ """
114
+ super().__init__(key, mentioned_list, mentioned_mobile_list, base_url, timeout, client_kwargs)
115
+ self.client = client or httpx.Client(**self.client_kwargs)
116
+
117
+ # --- 资源管理 ---
118
+
119
+ def close(self) -> None:
120
+ """关闭底层 HTTP 客户端,释放连接池。"""
121
+ self.client.close()
122
+
123
+ def __enter__(self) -> "API":
124
+ return self
125
+
126
+ def __exit__(self, *args: object) -> None:
127
+ self.close()
128
+
129
+ # --- 发送接口 ---
130
+
131
+ def send(self, payload: dict[str, Any] | None = None) -> GroupRobotSendResponse:
132
+ """发送消息。
133
+
134
+ Args:
135
+ payload: 符合企业微信机器人消息格式的请求体。
136
+
137
+ Returns:
138
+ 经过模型校验的发送响应,包含业务错误码和错误信息。
139
+
140
+ Raises:
141
+ httpx.HTTPStatusError: HTTP 状态码表示请求失败时抛出;业务错误需查看返回模型。
142
+
143
+ 官方文档:https://developer.work.weixin.qq.com/document/path/91770#消息类型及数据格式
144
+ """
145
+ response = self.client.post(
146
+ url=DEFAULT_SEND_URL,
147
+ json=payload or {},
148
+ params={"key": self.key},
149
+ )
150
+ response.raise_for_status()
151
+ return GroupRobotSendResponse.model_validate(response.json())
152
+
153
+ def send_text(
154
+ self,
155
+ content: str = "",
156
+ mentioned_list: list[str] | None = None,
157
+ mentioned_mobile_list: list[str] | None = None,
158
+ ) -> GroupRobotSendResponse:
159
+ """发送文本消息。
160
+
161
+ Args:
162
+ content: 文本内容。
163
+ mentioned_list: 本次额外 @ 的成员 userid 列表。
164
+ mentioned_mobile_list: 本次额外 @ 的成员手机号列表。
165
+
166
+ Returns:
167
+ 包含业务错误码和错误信息的发送响应模型。
168
+
169
+ 官方文档:https://developer.work.weixin.qq.com/document/path/91770#文本类型
170
+ """
171
+ return self.send(
172
+ {
173
+ "msgtype": "text",
174
+ "text": {
175
+ "content": content,
176
+ "mentioned_list": self.mentioned_list + (mentioned_list or []),
177
+ "mentioned_mobile_list": self.mentioned_mobile_list + (mentioned_mobile_list or []),
178
+ },
179
+ }
180
+ )
181
+
182
+ def send_markdown(self, content: str = "") -> GroupRobotSendResponse:
183
+ """发送 Markdown 消息。
184
+
185
+ Args:
186
+ content: Markdown 内容,最长 4096 个 UTF-8 字节。
187
+ Returns:
188
+ 发送响应模型,业务错误通过错误码和错误信息表示。
189
+ 官方文档:https://developer.work.weixin.qq.com/document/path/91770#markdown类型
190
+ """
191
+ return self.send({"msgtype": "markdown", "markdown": {"content": content}})
192
+
193
+ def send_markdown_v2(self, content: str = "") -> GroupRobotSendResponse:
194
+ """发送 Markdown V2 消息。
195
+
196
+ Args:
197
+ content: Markdown V2 内容,最长 4096 个 UTF-8 字节,不支持字体颜色和 @成员。
198
+ Returns:
199
+ 发送响应模型,业务错误通过错误码和错误信息表示。
200
+ 官方文档:https://developer.work.weixin.qq.com/document/path/91770#markdown-v2类型
201
+ """
202
+ return self.send({"msgtype": "markdown_v2", "markdown_v2": {"content": content}})
203
+
204
+ def send_image(self, base64: str, md5: str) -> GroupRobotSendResponse:
205
+ """发送图片消息。
206
+
207
+ Args:
208
+ base64: 图片二进制内容的 Base64 编码。
209
+ md5: 原始图片内容的 MD5;图片限 2MB,支持 JPG、PNG。
210
+ Returns:
211
+ 发送响应模型,业务错误通过错误码和错误信息表示。
212
+ 官方文档:https://developer.work.weixin.qq.com/document/path/91770#图片类型
213
+ """
214
+ return self.send({"msgtype": "image", "image": {"base64": base64, "md5": md5}})
215
+
216
+ def send_news(self, articles: list[dict[str, Any]]) -> GroupRobotSendResponse:
217
+ """发送 1 到 8 条图文,每条需包含 title、url,可选 description、picurl。
218
+
219
+ Args:
220
+ articles: 1 到 8 条图文文章数据。
221
+ Returns:
222
+ 发送响应模型,业务错误通过错误码和错误信息表示。
223
+ 官方文档:https://developer.work.weixin.qq.com/document/path/91770#图文类型
224
+ """
225
+ return self.send({"msgtype": "news", "news": {"articles": articles}})
226
+
227
+ def send_file(self, media_id: str) -> GroupRobotSendResponse:
228
+ """发送文件,media_id 由文件上传接口获取。
229
+
230
+ Args:
231
+ media_id: 文件上传接口返回的素材 ID。
232
+ Returns:
233
+ 发送响应模型,业务错误通过错误码和错误信息表示。
234
+ 官方文档:https://developer.work.weixin.qq.com/document/path/91770#文件类型
235
+ """
236
+ return self.send({"msgtype": "file", "file": {"media_id": media_id}})
237
+
238
+ def send_voice(self, media_id: str) -> GroupRobotSendResponse:
239
+ """发送语音,media_id 由语音上传接口获取。
240
+
241
+ Args:
242
+ media_id: 语音上传接口返回的素材 ID。
243
+ Returns:
244
+ 发送响应模型,业务错误通过错误码和错误信息表示。
245
+ 官方文档:https://developer.work.weixin.qq.com/document/path/91770#语音类型
246
+ """
247
+ return self.send({"msgtype": "voice", "voice": {"media_id": media_id}})
248
+
249
+ def send_template_card(self, template_card: dict[str, Any]) -> GroupRobotSendResponse:
250
+ """发送模板卡片,card_type 支持 text_notice 和 news_notice。
251
+
252
+ Args:
253
+ template_card: 模板卡片请求数据。
254
+ Returns:
255
+ 发送响应模型,业务错误通过错误码和错误信息表示。
256
+ 官方文档:https://developer.work.weixin.qq.com/document/path/91770#模版卡片类型
257
+ """
258
+ return self.send({"msgtype": "template_card", "template_card": template_card})
259
+
260
+ # --- 素材上传 ---
261
+
262
+ def upload_media(
263
+ self,
264
+ media: bytes | bytearray | str | Path | BinaryIO,
265
+ filename: str | None = None,
266
+ media_type: Literal["file", "voice"] = "file",
267
+ ) -> GroupRobotUploadMediaResponse:
268
+ """上传文件或语音素材。
269
+
270
+ Args:
271
+ media: 文件路径、字节内容或已打开的二进制文件对象。
272
+ filename: 可选文件名。
273
+ media_type: 上传类型,仅支持 file 或 voice。
274
+ Returns:
275
+ 包含 media_id 的上传响应模型;media_id 用于发送对应素材。
276
+ 注意:原图消息不通过此接口上传;文件对象从当前位置读取且不会被关闭。
277
+ 官方文档:https://developer.work.weixin.qq.com/document/path/91770#文件上传接口
278
+ """
279
+ response = self.client.post(
280
+ url=DEFAULT_UPLOAD_MEDIA_URL,
281
+ params={"key": self.key, "type": media_type},
282
+ files={"media": _extract_media(media, filename=filename)},
283
+ )
284
+ response.raise_for_status()
285
+ return GroupRobotUploadMediaResponse.model_validate(response.json())
286
+
287
+
288
+ class AsyncAPI(_BaseAPI):
289
+ """企业微信群机器人异步 API。
290
+
291
+ 官方文档:https://developer.work.weixin.qq.com/document/path/91770
292
+ """
293
+
294
+ def __init__(
295
+ self,
296
+ key: str | None = None,
297
+ mentioned_list: list[str] | None = None,
298
+ mentioned_mobile_list: list[str] | None = None,
299
+ base_url: str | None = None,
300
+ timeout: float | tuple[float, float, float, float] = 60.0,
301
+ client: httpx.AsyncClient | None = None,
302
+ client_kwargs: dict[str, Any] | None = None,
303
+ ):
304
+ """初始化异步客户端。
305
+
306
+ Args:
307
+ key: 群机器人的 Webhook key。
308
+ mentioned_list: 默认 @ 的成员 userid 列表。
309
+ mentioned_mobile_list: 默认 @ 的成员手机号列表。
310
+ base_url: API 根地址。
311
+ timeout: HTTP 请求超时时间。
312
+ client: 可选的已配置 httpx.AsyncClient。
313
+ client_kwargs: 未传入 client 时创建客户端的配置。
314
+
315
+ 官方文档:https://developer.work.weixin.qq.com/document/path/91770
316
+ """
317
+ super().__init__(key, mentioned_list, mentioned_mobile_list, base_url, timeout, client_kwargs)
318
+ self.client = client or httpx.AsyncClient(**self.client_kwargs)
319
+
320
+ # --- 资源管理 ---
321
+
322
+ async def close(self) -> None:
323
+ """关闭底层 HTTP 客户端,释放连接池。"""
324
+ await self.client.aclose()
325
+
326
+ async def __aenter__(self) -> "AsyncAPI":
327
+ return self
328
+
329
+ async def __aexit__(self, *args: object) -> None:
330
+ await self.close()
331
+
332
+ # --- 发送接口 ---
333
+
334
+ async def send(self, payload: dict[str, Any] | None = None) -> GroupRobotSendResponse:
335
+ """发送消息。
336
+
337
+ Args:
338
+ payload: 符合企业微信机器人消息格式的请求体。
339
+
340
+ Returns:
341
+ 经过模型校验的发送响应,包含业务错误码和错误信息。
342
+
343
+ Raises:
344
+ httpx.HTTPStatusError: HTTP 状态码表示请求失败时抛出;业务错误需查看返回模型。
345
+
346
+ 官方文档:https://developer.work.weixin.qq.com/document/path/91770#消息类型及数据格式
347
+ """
348
+ response = await self.client.post(
349
+ url=DEFAULT_SEND_URL,
350
+ json=payload or {},
351
+ params={"key": self.key},
352
+ )
353
+ response.raise_for_status()
354
+ return GroupRobotSendResponse.model_validate(response.json())
355
+
356
+ async def send_text(
357
+ self,
358
+ content: str = "",
359
+ mentioned_list: list[str] | None = None,
360
+ mentioned_mobile_list: list[str] | None = None,
361
+ ) -> GroupRobotSendResponse:
362
+ """异步发送文本消息。
363
+
364
+ Args:
365
+ content: 文本内容。
366
+ mentioned_list: 本次额外 @ 的成员 userid 列表。
367
+ mentioned_mobile_list: 本次额外 @ 的成员手机号列表。
368
+
369
+ Returns:
370
+ 包含业务错误码和错误信息的发送响应模型。
371
+
372
+ 官方文档:https://developer.work.weixin.qq.com/document/path/91770#文本类型
373
+ """
374
+ return await self.send(
375
+ {
376
+ "msgtype": "text",
377
+ "text": {
378
+ "content": content,
379
+ "mentioned_list": self.mentioned_list + (mentioned_list or []),
380
+ "mentioned_mobile_list": self.mentioned_mobile_list + (mentioned_mobile_list or []),
381
+ },
382
+ }
383
+ )
384
+
385
+ async def send_markdown(self, content: str = "") -> GroupRobotSendResponse:
386
+ """异步发送 Markdown 消息。
387
+
388
+ Args:
389
+ content: Markdown 内容,最长 4096 个 UTF-8 字节。
390
+ Returns:
391
+ 发送响应模型,业务错误通过错误码和错误信息表示。
392
+ 官方文档:https://developer.work.weixin.qq.com/document/path/91770#markdown类型
393
+ """
394
+ return await self.send({"msgtype": "markdown", "markdown": {"content": content}})
395
+
396
+ async def send_markdown_v2(self, content: str = "") -> GroupRobotSendResponse:
397
+ """异步发送 Markdown V2 消息。
398
+
399
+ Args:
400
+ content: Markdown V2 内容,最长 4096 个 UTF-8 字节,不支持字体颜色和 @成员。
401
+ Returns:
402
+ 发送响应模型,业务错误通过错误码和错误信息表示。
403
+ 官方文档:https://developer.work.weixin.qq.com/document/path/91770#markdown-v2类型
404
+ """
405
+ return await self.send({"msgtype": "markdown_v2", "markdown_v2": {"content": content}})
406
+
407
+ async def send_image(self, base64: str, md5: str) -> GroupRobotSendResponse:
408
+ """异步发送图片消息。
409
+
410
+ Args:
411
+ base64: 图片二进制内容的 Base64 编码。
412
+ md5: 原始图片内容的 MD5;图片限 2MB,支持 JPG、PNG。
413
+ Returns:
414
+ 发送响应模型,业务错误通过错误码和错误信息表示。
415
+ 官方文档:https://developer.work.weixin.qq.com/document/path/91770#图片类型
416
+ """
417
+ return await self.send({"msgtype": "image", "image": {"base64": base64, "md5": md5}})
418
+
419
+ async def send_news(self, articles: list[dict[str, Any]]) -> GroupRobotSendResponse:
420
+ """异步发送 1 到 8 条图文,每条需包含 title、url,可选 description、picurl。
421
+
422
+ Args:
423
+ articles: 1 到 8 条图文文章数据。
424
+ Returns:
425
+ 发送响应模型,业务错误通过错误码和错误信息表示。
426
+ 官方文档:https://developer.work.weixin.qq.com/document/path/91770#图文类型
427
+ """
428
+ return await self.send({"msgtype": "news", "news": {"articles": articles}})
429
+
430
+ async def send_file(self, media_id: str) -> GroupRobotSendResponse:
431
+ """异步发送文件,media_id 由文件上传接口获取。
432
+
433
+ Args:
434
+ media_id: 文件上传接口返回的素材 ID。
435
+ Returns:
436
+ 发送响应模型,业务错误通过错误码和错误信息表示。
437
+ 官方文档:https://developer.work.weixin.qq.com/document/path/91770#文件类型
438
+ """
439
+ return await self.send({"msgtype": "file", "file": {"media_id": media_id}})
440
+
441
+ async def send_voice(self, media_id: str) -> GroupRobotSendResponse:
442
+ """异步发送语音,media_id 由语音上传接口获取。
443
+
444
+ Args:
445
+ media_id: 语音上传接口返回的素材 ID。
446
+ Returns:
447
+ 发送响应模型,业务错误通过错误码和错误信息表示。
448
+ 官方文档:https://developer.work.weixin.qq.com/document/path/91770#语音类型
449
+ """
450
+ return await self.send({"msgtype": "voice", "voice": {"media_id": media_id}})
451
+
452
+ async def send_template_card(self, template_card: dict[str, Any]) -> GroupRobotSendResponse:
453
+ """异步发送模板卡片,card_type 支持 text_notice 和 news_notice。
454
+
455
+ Args:
456
+ template_card: 模板卡片请求数据。
457
+ Returns:
458
+ 发送响应模型,业务错误通过错误码和错误信息表示。
459
+ 官方文档:https://developer.work.weixin.qq.com/document/path/91770#模版卡片类型
460
+ """
461
+ return await self.send({"msgtype": "template_card", "template_card": template_card})
462
+
463
+ # --- 素材上传 ---
464
+
465
+ async def upload_media(
466
+ self,
467
+ media: bytes | bytearray | str | Path | BinaryIO,
468
+ filename: str | None = None,
469
+ media_type: Literal["file", "voice"] = "file",
470
+ ) -> GroupRobotUploadMediaResponse:
471
+ """上传文件或语音素材。
472
+
473
+ Args:
474
+ media: 文件路径、字节内容或已打开的二进制文件对象。
475
+ filename: 可选文件名。
476
+ media_type: 上传类型,仅支持 file 或 voice。
477
+ Returns:
478
+ 包含 media_id 的上传响应模型;media_id 用于发送对应素材。
479
+ 注意:原图消息不通过此接口上传;文件对象从当前位置读取且不会被关闭。
480
+ 官方文档:https://developer.work.weixin.qq.com/document/path/91770#文件上传接口
481
+ """
482
+ response = await self.client.post(
483
+ url=DEFAULT_UPLOAD_MEDIA_URL,
484
+ params={"key": self.key, "type": media_type},
485
+ files={"media": _extract_media(media, filename=filename)},
486
+ )
487
+ response.raise_for_status()
488
+ return GroupRobotUploadMediaResponse.model_validate(response.json())
@@ -0,0 +1,50 @@
1
+ """群机器人消息发送与素材上传响应模型。
2
+
3
+ 官方文档:https://developer.work.weixin.qq.com/document/path/91770
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from pydantic import BaseModel, ConfigDict
9
+
10
+ __all__ = ["BaseResponse", "SendResponse", "UploadMediaResponse"]
11
+
12
+
13
+ class BaseResponse(BaseModel):
14
+ """企业微信业务响应,errcode 为 0 表示成功,errmsg 为结果说明。
15
+
16
+ 官方文档:https://developer.work.weixin.qq.com/document/path/91770
17
+ """
18
+
19
+ model_config = ConfigDict(extra="allow", populate_by_name=True)
20
+ errcode: int
21
+ errmsg: str
22
+
23
+ @property
24
+ def ok(self) -> bool:
25
+ """判断 errcode 是否为 0。"""
26
+ return self.errcode == 0
27
+
28
+
29
+ class SendResponse(BaseResponse):
30
+ """消息发送响应,通过 ok 判断企业微信业务请求是否成功。
31
+
32
+ 官方文档:https://developer.work.weixin.qq.com/document/path/91770#消息类型及数据格式
33
+ """
34
+
35
+
36
+ class UploadMediaResponse(BaseResponse):
37
+ """素材上传响应,包含素材 ID、类型和创建时间。
38
+
39
+ media_id 用于后续文件或语音消息发送;素材仅供对应机器人使用,有效期为 3 天。
40
+ 官方文档:https://developer.work.weixin.qq.com/document/path/91770#文件上传接口
41
+ """
42
+
43
+ media_id: str
44
+ type: str
45
+ created_at: int
46
+
47
+ @property
48
+ def ok(self) -> bool:
49
+ """判断业务请求是否成功且 media_id 非空。"""
50
+ return super().ok and bool(self.media_id)
@@ -0,0 +1 @@
1
+ from __future__ import annotations
@@ -0,0 +1 @@
1
+ from __future__ import annotations
@@ -0,0 +1 @@
1
+ from __future__ import annotations
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from .core.group_robot import GroupRobotAPI, GroupRobotAsyncAPI
6
+
7
+
8
+ class APIProvider:
9
+ """企业微信提供器."""
10
+
11
+ def __init__(self):
12
+ pass
13
+
14
+ @staticmethod
15
+ def group_robot_api(*args, **kwargs) -> GroupRobotAPI:
16
+ return GroupRobotAPI(*args, **kwargs)
17
+
18
+ @staticmethod
19
+ def group_robot_async_api(*args, **kwargs) -> GroupRobotAsyncAPI:
20
+ return GroupRobotAsyncAPI(*args, **kwargs)
@@ -0,0 +1,323 @@
1
+ Metadata-Version: 2.3
2
+ Name: py-wecom-sdk
3
+ Version: 0.1.0
4
+ Summary: 企业微信(WeCom)开放接口的 Python SDK,基于 httpx + pydantic,同步与异步统一封装。
5
+ Keywords: wecom,wechat-work,企业微信,sdk,httpx,pydantic,群机器人
6
+ Author: guolei
7
+ Author-email: guolei <174000902@qq.com>
8
+ License: MIT License
9
+
10
+ Copyright (c) 2026 郭磊
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in all
20
+ copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
+ SOFTWARE.
29
+ Classifier: Development Status :: 3 - Alpha
30
+ Classifier: Intended Audience :: Developers
31
+ Classifier: License :: OSI Approved :: MIT License
32
+ Classifier: Operating System :: OS Independent
33
+ Classifier: Programming Language :: Python :: 3
34
+ Classifier: Programming Language :: Python :: 3.10
35
+ Classifier: Programming Language :: Python :: 3.11
36
+ Classifier: Programming Language :: Python :: 3.12
37
+ Classifier: Programming Language :: Python :: 3.13
38
+ Classifier: Programming Language :: Python :: 3 :: Only
39
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
40
+ Classifier: Typing :: Typed
41
+ Requires-Dist: anyio>=4.16.0
42
+ Requires-Dist: httpx>=0.28.1
43
+ Requires-Dist: pydantic>=2.14.3
44
+ Requires-Dist: diskcache>=5.6.0 ; extra == 'all'
45
+ Requires-Dist: redis>=5.0.0 ; extra == 'all'
46
+ Requires-Dist: diskcache>=5.6.0 ; extra == 'diskcache'
47
+ Requires-Dist: redis>=5.0.0 ; extra == 'redis'
48
+ Maintainer: guolei
49
+ Maintainer-email: guolei <174000902@qq.com>
50
+ Requires-Python: >=3.10
51
+ Project-URL: Homepage, https://gitee.com/guolei19850528/py_wecom_sdk
52
+ Project-URL: Repository, https://gitee.com/guolei19850528/py_wecom_sdk.git
53
+ Project-URL: Issues, https://gitee.com/guolei19850528/py_wecom_sdk/issues
54
+ Provides-Extra: all
55
+ Provides-Extra: diskcache
56
+ Provides-Extra: redis
57
+ Description-Content-Type: text/markdown
58
+
59
+ # py_wecom_sdk
60
+
61
+ [![Python](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/)
62
+ [![License](https://img.shields.io/badge/license-MIT-green)](https://opensource.org/licenses/MIT)
63
+ [![httpx](https://img.shields.io/badge/httpx-0.28%2B-orange)](https://github.com/encode/httpx)
64
+ [![pydantic](https://img.shields.io/badge/pydantic-2.14%2B-emerald)](https://github.com/pydantic/pydantic)
65
+
66
+ 企业微信(WeCom)开放接口的 Python SDK,基于 **httpx + pydantic v2**,**同步 / 异步统一封装**。
67
+
68
+ > 官方文档:<https://developer.work.weixin.qq.com/document/path/91770>
69
+
70
+ ---
71
+
72
+ ## ✨ 特性
73
+
74
+ - **同步 / 异步统一 API** — `GroupRobotAPI` 与 `GroupRobotAsyncAPI` 接口完全一致,只需替换 import
75
+ - **类型安全** — pydantic v2 自动校验所有请求 / 响应,Mypy strict 模式 100% 通过
76
+ - **零魔法基类** — 同步 / 异步共享 `_BaseAPI`,初始化和资源管理逻辑一次实现
77
+ - **丰富的消息类型** — 文本、Markdown、Markdown V2、图片、图文、文件、语音、模板卡片
78
+ - **素材上传支持** — `upload_media()` 统一处理文件 / 语音上传并返回 `media_id`
79
+ - **HTTP 连接池管理** — `close()` / `with` / `async with` 三种方式释放连接
80
+ - **企业级依赖** — httpx(encode 维护)+ pydantic v2(比 v1 快 5-50 倍)
81
+
82
+ ## 📦 安装
83
+
84
+ ### 使用 pip
85
+
86
+ ```bash
87
+ pip install py-wecom-sdk
88
+ ```
89
+
90
+ ### 使用 uv(推荐)
91
+
92
+ ```bash
93
+ uv add py-wecom-sdk
94
+ ```
95
+
96
+ ### 安装可选依赖
97
+
98
+ ```bash
99
+ # DiskCache 本地缓存后端
100
+ uv add "py-wecom-sdk[diskcache]"
101
+
102
+ # Redis 分布式缓存后端
103
+ uv add "py-wecom-sdk[redis]"
104
+
105
+ # 全部可选依赖
106
+ uv add "py-wecom-sdk[all]"
107
+ ```
108
+
109
+ ## 🚀 快速开始
110
+
111
+ ### 同步客户端
112
+
113
+ ```python
114
+ from py_wecom_sdk import WeComAPIProvider
115
+
116
+ api = WeComAPIProvider.group_robot_api(key="YOUR_WEBHOOK_KEY")
117
+
118
+ # 发送文本
119
+ resp = api.send_text("Hello, WeCom!")
120
+ assert resp.ok
121
+
122
+ # 发送 Markdown
123
+ api.send_markdown("## 标题\n> <font color='info'>重要通知</font>")
124
+
125
+ # 发送图文
126
+ api.send_news([{"title": "标题", "url": "https://example.com"}])
127
+
128
+ # 发送文件(需先上传)
129
+ uploaded = api.upload_media("/path/to/file.pdf", media_type="file")
130
+ api.send_file(uploaded.media_id)
131
+
132
+ # 释放连接池
133
+ api.close()
134
+ # 或使用上下文管理器:
135
+ # with WeComAPIProvider.group_robot_api(key="YOUR_KEY") as api: ...
136
+ ```
137
+
138
+ ### 异步客户端
139
+
140
+ ```python
141
+ import asyncio
142
+ from py_wecom_sdk import WeComAPIProvider
143
+
144
+ async def main():
145
+ async with WeComAPIProvider.group_robot_async_api(key="YOUR_WEBHOOK_KEY") as api:
146
+ resp = await api.send_text("Hello from async!")
147
+ assert resp.ok
148
+ await api.send_markdown_v2("**Markdown V2** 也很好用")
149
+ uploaded = await api.upload_media(b"fake data", filename="demo.txt", media_type="file")
150
+ await api.send_file(uploaded.media_id)
151
+
152
+ asyncio.run(main())
153
+ ```
154
+
155
+ ### 通过原始 dict 发送任意消息类型
156
+
157
+ 所有便捷方法底层都调用 `send(payload)`,你可以直接传入符合企业微信协议的字典:
158
+
159
+ ```python
160
+ api.send({"msgtype": "text", "text": {"content": "原始 dict", "mentioned_list": ["userid1"]}})
161
+ ```
162
+
163
+ ## 📖 功能清单
164
+
165
+ ### `GroupRobotAPI` / `GroupRobotAsyncAPI`
166
+
167
+ | 方法 | 功能 | 官方文档 |
168
+ |------|------|----------|
169
+ | `send(payload)` | 发送任意类型消息(原始 dict) | — |
170
+ | `send_text(content, mentioned_list, mentioned_mobile_list)` | 文本消息,支持 @ 指定成员 | [文本类型](https://developer.work.weixin.qq.com/document/path/91770#文本类型) |
171
+ | `send_markdown(content)` | Markdown 消息 | [Markdown 类型](https://developer.work.weixin.qq.com/document/path/91770#markdown类型) |
172
+ | `send_markdown_v2(content)` | Markdown V2 消息 | [Markdown V2](https://developer.work.weixin.qq.com/document/path/91770#markdown-v2类型) |
173
+ | `send_image(base64, md5)` | 图片消息(限 2MB,JPG / PNG) | [图片类型](https://developer.work.weixin.qq.com/document/path/91770#图片类型) |
174
+ | `send_news(articles)` | 图文消息(1-8 条) | [图文类型](https://developer.work.weixin.qq.com/document/path/91770#图文类型) |
175
+ | `send_file(media_id)` | 文件消息(需先上传) | [文件类型](https://developer.work.weixin.qq.com/document/path/91770#文件类型) |
176
+ | `send_voice(media_id)` | 语音消息(需先上传) | [语音类型](https://developer.work.weixin.qq.com/document/path/91770#语音类型) |
177
+ | `send_template_card(card)` | 模板卡片(text_notice / news_notice) | [模板卡片](https://developer.work.weixin.qq.com/document/path/91770#模版卡片类型) |
178
+ | `upload_media(media, filename, media_type)` | 上传文件 / 语音素材,返回 `media_id` | [文件上传接口](https://developer.work.weixin.qq.com/document/path/91770#文件上传接口) |
179
+
180
+ ### `send_text` 默认 @ 配置
181
+
182
+ 实例化时可传入默认 @ 列表,每次 `send_text` 会自动合并:
183
+
184
+ ```python
185
+ api = WeComAPIProvider.group_robot_api(
186
+ key="YOUR_KEY",
187
+ mentioned_list=["userid1"], # 每次自动 @ userid1
188
+ mentioned_mobile_list=["13800138000"],
189
+ )
190
+ api.send_text("重要通知", mentioned_list=["userid2"]) # 合并 @ userid1 + userid2
191
+ ```
192
+
193
+ ### `UploadMediaResponse.ok` 多一层校验
194
+
195
+ 上传响应除了业务成功(`errcode == 0`),还额外校验 `media_id` 非空:
196
+
197
+ ```python
198
+ uploaded = api.upload_media("/path/to/file.pdf", media_type="file")
199
+ if not uploaded.ok:
200
+ print(f"上传失败:errcode={uploaded.errcode}, errmsg={uploaded.errmsg}")
201
+ ```
202
+
203
+ ## 🏗️ 项目结构
204
+
205
+ ```
206
+ py_wecom_sdk/
207
+ ├── pyproject.toml # 项目配置(PEP 621 + Ruff + Mypy + Pytest)
208
+ ├── LICENSE # MIT License
209
+ ├── README.md # 中文文档
210
+ ├── README.en.md # 英文文档
211
+ ├── .python-version # 锁定 Python 版本(3.10)
212
+ ├── .gitignore
213
+ ├── .env.example # 示例 .env 文件
214
+ │
215
+ ├── src/
216
+ │ └── py_wecom_sdk/ # 包根目录(src-layout)
217
+ │ ├── __init__.py # 导出 WeComAPIProvider
218
+ │ ├── provider.py # APIProvider 统一入口
219
+ │ └── core/
220
+ │ ├── group_robot/ # 群机器人模块 ✅ 已实现
221
+ │ │ ├── __init__.py # 公共导出
222
+ │ │ ├── _api.py # GroupRobotAPI + GroupRobotAsyncAPI + _BaseAPI
223
+ │ │ └── _models.py # SendResponse / UploadMediaResponse / BaseResponse
224
+ │ └── token/ # access_token 模块 🚧 预留
225
+ │ ├── __init__.py
226
+ │ ├── _client.py
227
+ │ └── _models.py
228
+ │
229
+ ├── examples/
230
+ │ └── group_robot.py # 群机器人完整示例(同步 + 异步 + 多消息类型)
231
+ │
232
+ └── tests/
233
+ ├── conftest.py
234
+ └── test_group_robot.py # 单元测试(respx mock)
235
+ ```
236
+
237
+ ## 🛠️ 开发
238
+
239
+ ### 环境准备
240
+
241
+ ```bash
242
+ # 克隆仓库
243
+ git clone https://gitee.com/guolei19850528/py_wecom_sdk.git
244
+ cd py_wecom_sdk
245
+
246
+ # 安装 uv(包管理器)
247
+ curl -LsSf https://astral.sh/uv/install.sh | sh
248
+
249
+ # 创建虚拟环境 + 安装全部依赖(含 dev)
250
+ uv sync --group dev
251
+ ```
252
+
253
+ ### 代码质量
254
+
255
+ ```bash
256
+ # Ruff:lint(修复) + format
257
+ uv run ruff check --fix src tests
258
+ uv run ruff format src tests
259
+
260
+ # Mypy(strict 模式)
261
+ uv run mypy src
262
+ ```
263
+
264
+ ### 测试
265
+
266
+ ```bash
267
+ # 运行全部测试(离线,使用 respx mock)
268
+ uv run pytest
269
+
270
+ # 运行需要真实网络的集成测试
271
+ cp .env.example .env
272
+ # 编辑 .env 填入 WECOM_GROUP_ROBOT_API_KEY
273
+ uv run pytest -m real_network
274
+ ```
275
+
276
+ ### 示例运行
277
+
278
+ ```bash
279
+ # 编辑 .env 填入机器人 key
280
+ cp .env.example .env
281
+ vim .env
282
+
283
+ # 同步发送
284
+ uv run python examples/group_robot.py
285
+
286
+ # 异步发送 + 图片 / 文件 / 语音
287
+ uv run python examples/group_robot.py --async --image ./test.png --file ./report.pdf
288
+ ```
289
+
290
+ ### 构建发布
291
+
292
+ ```bash
293
+ # 构建 wheel + sdist
294
+ uv build
295
+
296
+ # 发布到 PyPI / Gitee
297
+ uv publish --repository gitee
298
+ ```
299
+
300
+ ## 📋 路线图
301
+
302
+ - [x] 群机器人模块(group_robot)
303
+ - [x] 同步 / 异步统一 API
304
+ - [x] 全部 9 种消息类型
305
+ - [x] 素材上传接口
306
+ - [x] pydantic 数据模型校验
307
+ - [ ] access_token 模块(token)
308
+ - [ ] 缓存后端:内存 / DiskCache / Redis
309
+ - [ ] 自动刷新 + 并发去重
310
+ - [ ] 通讯录模块(contact)
311
+ - [ ] 消息推送模块(message)
312
+
313
+ ## 📄 许可证
314
+
315
+ 本项目基于 **MIT License** 开源,详见 [LICENSE](LICENSE)。
316
+
317
+ ## 📎 相关链接
318
+
319
+ - [企业微信开发者中心](https://developer.work.weixin.qq.com/)
320
+ - [群机器人接口文档](https://developer.work.weixin.qq.com/document/path/91770)
321
+ - [httpx 官方仓库](https://github.com/encode/httpx)
322
+ - [pydantic v2 文档](https://docs.pydantic.dev/)
323
+ - [uv 官方仓库](https://github.com/astral-sh/uv)
@@ -0,0 +1,11 @@
1
+ py_wecom_sdk/__init__.py,sha256=yS7F1ZeXNvkvpUwFDf17W8seCsIsF_r4ZDRTlb5gpeo,128
2
+ py_wecom_sdk/core/group_robot/__init__.py,sha256=g6-6HRst5jqsX1GeK09eYnai0UCEQlSGZwVuFiJwqUY,600
3
+ py_wecom_sdk/core/group_robot/_api.py,sha256=kMwVVokC12VBKpOaVU-o3qezcdAIEAs8gk0JDpM9Dlo,19796
4
+ py_wecom_sdk/core/group_robot/_models.py,sha256=NO03LpuN6UajbE8KBTQ-f9EOfgQ0nAmXtw4moebSW3o,1484
5
+ py_wecom_sdk/core/token/__init__.py,sha256=U4S_2y3zgLZVfMenHRaJFBW8yqh2mUBuI291LGQVOJ8,35
6
+ py_wecom_sdk/core/token/_client.py,sha256=U4S_2y3zgLZVfMenHRaJFBW8yqh2mUBuI291LGQVOJ8,35
7
+ py_wecom_sdk/core/token/_models.py,sha256=U4S_2y3zgLZVfMenHRaJFBW8yqh2mUBuI291LGQVOJ8,35
8
+ py_wecom_sdk/provider.py,sha256=wwJ9t0MbRtiA4PUrWl16H7bgpBZ4U6w9V89iKxorcq4,480
9
+ py_wecom_sdk-0.1.0.dist-info/WHEEL,sha256=MWx7psoF7KNST9G72pGtiBYuaXRXQXBk9pw5R6FzYko,80
10
+ py_wecom_sdk-0.1.0.dist-info/METADATA,sha256=3Tm95H1-f7n-5QrHPV7m9tZtcfIH7MUc3KJe9-uUUjU,11755
11
+ py_wecom_sdk-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.13.7
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any