Metadata-Version: 2.4
Name: py-lmobile-toolkit
Version: 3.0.1
Summary: 乐信（51welink）短信服务 Python SDK，提供同步和异步接口调用，支持签名认证
Author-email: Guolei <174000902@qq.com>
License: MIT License
        
        Copyright (c) 2026 郭磊
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
Project-URL: Homepage, https://gitee.com/guolei19850528/py_lmobile_toolkit
Project-URL: Repository, https://gitee.com/guolei19850528/py_lmobile_toolkit.git
Project-URL: Documentation, https://www.lmobile.cn/ApiPages/index.html
Keywords: lmobile,51welink,python,client,api,短信,sms,乐信
Classifier: License :: OSI Approved :: MIT License
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Operating System :: OS Independent
Classifier: Topic :: Communications
Classifier: Topic :: Communications :: Telephony
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx>=0.24.0
Requires-Dist: pydantic>=2.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
Requires-Dist: python-dotenv>=1.0.0; extra == "dev"
Dynamic: license-file

# py-lmobile-toolkit

乐信（51welink）短信服务 Python SDK，提供同步与异步客户端，支持 MD5 + SHA256 签名认证，响应模型基于 Pydantic v2。

<p align="center">
  <a href="https://pypi.org/project/py-lmobile-toolkit/">
    <img src="https://badge.fury.io/py/py-lmobile-toolkit.svg" alt="PyPI version">
  </a>
  <a href="https://pypi.org/project/py-lmobile-toolkit/">
    <img src="https://img.shields.io/pypi/pyversions/py-lmobile-toolkit.svg" alt="Python versions">
  </a>
  <a href="LICENSE">
    <img src="https://img.shields.io/github/license/guolei19850528/py_lmobile_toolkit.svg" alt="License">
  </a>
  <a href="https://github.com/guolei19850528/py_lmobile_toolkit/issues">
    <img src="https://img.shields.io/github/issues/guolei19850528/py_lmobile_toolkit.svg" alt="Issues">
  </a>
</p>

<p align="center">
  <a href="README.en.md">English</a> | 中文
</p>

---

## 项目简介

`py-lmobile-toolkit` 是乐信（51welink）短信开放平台的非官方 Python 客户端库，封装了官方 HTTP API，提供同步客户端 `SmsClient` 和异步客户端 `AsyncSmsClient`，两者方法名完全一致，异步客户端支持 `async with` 上下文管理器。

- 项目主页：https://gitee.com/guolei19850528/py_lmobile_toolkit
- PyPI：https://pypi.org/project/py-lmobile-toolkit/
- 官方 API 文档：https://www.lmobile.cn/ApiPages/index.html
- 许可证：[MIT License](LICENSE)

## 功能特性

- ✅ 同步客户端 `SmsClient` 与异步客户端 `AsyncSmsClient`（方法名完全一致）
- ✅ 异步客户端支持 `async with` 上下文管理器
- ✅ 请求签名认证机制（MD5 密码 + SHA256 签名，AccessKey）
- ✅ 响应数据模型验证（Pydantic v2），所有模型开启 `extra="allow"`，兼容 API 扩展字段
- ✅ 覆盖官方文档导航中的全部客户端可调用接口（短信/模板短信/账号/其他 API）
- ✅ 接口端点以常量集中管理，便于按官方文档精确对齐
- ✅ 所有方法支持透传 `**kwargs` 给 httpx，方便覆盖 URL、请求头等
- ✅ 基于 Python 3.10+，依赖 `httpx>=0.24.0` 与 `pydantic>=2.0`

> **注意**：账号、密码、产品 ID 等敏感信息请通过环境变量（`.env` 文件）配置，勿硬编码在代码中。

## 安装

### 推荐：使用 uv 安装

```bash
uv add py-lmobile-toolkit
```

或从本地源码安装（开发模式）：

```bash
uv pip install -e .
```

安装测试依赖：

```bash
uv pip install -e ".[dev]"
```

### 或者使用 pip 安装

```bash
pip install py-lmobile-toolkit
```

或从本地源码安装（开发模式）：

```bash
pip install -e .
```

安装测试依赖：

```bash
pip install -e ".[dev]"
```

## 快速开始

### 同步调用

```python
from py_lmobile_toolkit.sms.client import SmsClient

# 创建同步客户端
sms = SmsClient(
    account_id="your_account_id",
    password="your_password",
    product_id="your_product_id",
)

# 发送单条短信
response = sms.send_sms(
    phone_nos="13800138000",
    content="【乐信】您的验证码是：123456，请于 5 分钟内正确输入",
)
print(f"成功: {response.ok}, 结果码: {response.Result}")

sms.close()
```

也可使用上下文管理器自动关闭客户端：

```python
with SmsClient(
    account_id="your_account_id",
    password="your_password",
    product_id="your_product_id",
) as sms:
    response = sms.send_sms(
        phone_nos="13800138000",
        content="【乐信】您的验证码是：123456",
    )
    print(response.ok, response.Result)
```

### 异步调用

```python
import asyncio
from py_lmobile_toolkit.sms.client import AsyncSmsClient


async def main():
    async with AsyncSmsClient(
        account_id="your_account_id",
        password="your_password",
        product_id="your_product_id",
    ) as sms:
        response = await sms.send_sms(
            phone_nos=["13800138000", "13900139000"],
            content="【乐信】批量发送测试",
        )
        print(f"成功: {response.ok}, 结果码: {response.Result}")


asyncio.run(main())
```

## 已实现的接口

| 分类 | 接口 | 同步方法 | 异步方法 | 响应模型 | 端点状态 |
|------|------|----------|----------|----------|----------|
| 短信API | 提交短信 | `send_sms` / `send` | `send_sms` / `send` | `SmsSendSmsResponse` | ✅ 已知可靠 |
| 短信API | 提交个性化短信(变量XML格式) | `send_personalized_xml` | `send_personalized_xml` | `SmsBaseResponse` | [待核实] |
| 短信API | 提交个性化短信(变量Json格式) | `send_personalized_json` | `send_personalized_json` | `SmsBaseResponse` | [待核实] |
| 短信API | 拉取状态报告 | `pull_status_report` | `pull_status_report` | `SmsPullReportResponse` | [待核实] |
| 短信API | 拉取上行信息 | `pull_mo_message` | `pull_mo_message` | `SmsPullMoResponse` | [待核实] |
| 短信API | 取消定时消息发送 | `cancel_scheduled_sms` | `cancel_scheduled_sms` | `SmsCancelScheduledResponse` | [待核实] |
| 模板短信API | 提交模板短信 | `submit_template_sms` | `submit_template_sms` | `SmsSubmitTemplateSmsResponse` | [待核实] |
| 模板短信API | 提交个性化模板短信 | `submit_personalized_template_sms` | `submit_personalized_template_sms` | `SmsBaseResponse` | [待核实] |
| 模板短信API | 短信模板报备 | `register_template` | `register_template` | `SmsRegisterTemplateResponse` | [待核实] |
| 模板短信API | 视频短信模板报备 | `register_video_template` | `register_video_template` | `SmsRegisterVideoTemplateResponse` | [待核实] |
| 模板短信API | 删除模板 | `delete_template` | `delete_template` | `SmsDeleteTemplateResponse` | [待核实] |
| 模板短信API | 查询模板 | `query_template` | `query_template` | `SmsQueryTemplateResponse` | [待核实] |
| 账号API | 查询余量 | `query_balance` | `query_balance` | `SmsBalanceResponse` | [待核实] |
| 账号API | 查询计费量 | `query_billing` | `query_billing` | `SmsBillingResponse` | [待核实] |
| 其他API | H5跳转小程序短链生成 | `generate_h5_miniprogram_short_link` | `generate_h5_miniprogram_short_link` | `SmsH5ShortLinkResponse` | [待核实] |
| 其他API | 更新H5跳小程序的AccessToken | `update_h5_miniprogram_access_token` | `update_h5_miniprogram_access_token` | `SmsUpdateAccessTokenResponse` | [待核实] |

**回调类接口**（状态报告推送、上行推送、模板审核结果推送）为服务端 Webhook，非客户端调用接口，此处不提供客户端方法。如需接收推送，请在业务服务器上实现对应 HTTP 接口并解析请求体。

### 端点说明

- `提交短信` 端点 `/EncryptionSubmit/SendSms.ashx` 为已知可靠（与历史版本一致）。
- 其余接口端点基于官方文档接口名称推断，均标注 `[待核实]`，定义在 `src/py_lmobile_toolkit/sms/client.py` 的顶部常量中，请以 https://www.lmobile.cn/ApiPages/index.html 实际文档的请求地址与参数为准进行调整。
- 每个方法均接受 `url` 关键字参数可覆盖默认端点，亦可通过 `**kwargs` 透传 httpx 参数。

## 响应模型

所有响应均继承 `SmsBaseResponse`，包含 `Result` 字段，`Result` 为 `"succ"` 表示成功，可通过 `ok` 属性判断：

```python
from py_lmobile_toolkit.sms.models import SmsSendSmsResponse

response: SmsSendSmsResponse = sms.send_sms(...)
if response.ok:
    print("发送成功")
else:
    print(f"发送失败，结果码: {response.Result}")
```

所有模型均开启 `extra="allow"`，兼容 API 返回的扩展字段。

## 签名生成算法

签名生成遵循以下步骤：

1. **密码 MD5 加密**：`MD5(password + smms_encrypt_key)`，结果转大写
2. **参数拼接**（按顺序）：
   ```
   AccountId={account_id}&PhoneNos={first_phone}&Password={md5_password}&Random={random}&Timestamp={timestamp}
   ```
   注意：`PhoneNos` 只取第一个手机号用于签名
3. **SHA256 加密**：对拼接后的字符串进行 SHA256 哈希，作为 `AccessKey`

```python
from py_lmobile_toolkit.sms.auth import timestamp, random_digits, sha256_signature

ts = timestamp()
digits = random_digits()
signature = sha256_signature(
    account_id="your_account_id",
    password="your_password",
    phone_nos="13800138000",
    random_digits=digits,
    timestamp=ts,
    smms_encrypt_key="SMmsEncryptKey",
)
```

## 示例代码 (`examples/`)

完整可运行示例位于 `examples/sms/`，文件命名规则为 `example_*.py`（同步）与 `example_async_*.py`（异步）。

运行示例前，请复制模板并填写真实账号：

```bash
cp examples/sms/.env.example examples/sms/.env
```

| 同步示例 | 异步示例 | 说明 |
|----------|----------|------|
| `example_send_sms.py` | `example_async_send_sms.py` | 提交短信 |
| `example_pull_report.py` | `example_async_pull_report.py` | 拉取状态报告 |
| `example_query_balance.py` | `example_async_query_balance.py` | 查询余量 |
| `example_submit_template_sms.py` | `example_async_submit_template_sms.py` | 提交模板短信 |
| `example_send_with_config.py` | — | 带配置发送短信 |
| `example_send_test_cases.py` | — | 发送测试用例 |

## 测试 (`tests/`)

测试用例位于 `tests/sms/`，基于 pytest + pytest-asyncio，统一从 `tests/sms/.env` 读取配置，公共夹具定义在 `tests/sms/conftest.py`。

运行测试前，请复制模板并填写真实账号：

```bash
cp tests/sms/.env.example tests/sms/.env
```

安装测试依赖并运行：

```bash
uv pip install -e ".[dev]"
pytest tests/sms -q
```

未配置 `SMS_ACCOUNT_ID` / `SMS_PASSWORD` 时，测试会自动跳过。

## 目录结构

```text
py_lmobile_toolkit/
├── README.md                 # 中文文档（本文件）
├── README.en.md              # 英文文档
├── pyproject.toml            # 项目元数据与依赖
├── LICENSE                   # MIT 许可证
├── .gitignore                # Git 忽略规则
├── src/
│   └── py_lmobile_toolkit/
│       ├── __init__.py       # 顶层包
│       ├── py.typed          # PEP 561 类型标记
│       └── sms/              # 短信子包
│           ├── __init__.py
│           ├── auth.py       # 签名生成工具（md5/sha256/timestamp/random_digits）
│           ├── client.py     # SmsClient（同步）与 AsyncSmsClient（异步）
│           └── models.py     # pydantic 响应模型
├── tests/
│   └── sms/
│       ├── __init__.py
│       ├── conftest.py       # 公共夹具（客户端、查询条件）
│       ├── .env.example      # 测试配置模板
│       └── test_*.py         # 各接口的同步 / 异步测试用例
└── examples/
    └── sms/
        ├── .env.example      # 示例配置模板
        ├── example_common.py # 示例公共辅助（读取配置、构造客户端）
        ├── example_*.py      # 各接口同步示例
        └── example_async_*.py # 各接口异步示例
```

## 环境变量配置

示例与测试均通过 `.env` 文件读取以下环境变量：

| 变量名 | 说明 | 默认值 |
|--------|------|--------|
| `SMS_BASE_URL` | API 基础地址 | `https://api.51welink.com/` |
| `SMS_ACCOUNT_ID` | 账号 ID（必填） | — |
| `SMS_PASSWORD` | 账号密码（必填） | — |
| `SMS_PRODUCT_ID` | 产品 ID | `""` |
| `SMS_SMMS_ENCRYPT_KEY` | 加密密钥（MD5 盐值） | `SMmsEncryptKey` |

## 贡献指南

欢迎提交 Issue 和 Pull Request。在提交代码前，请：

1. Fork 本仓库并创建特性分支
2. 编写或更新对应的测试用例
3. 确保 `pytest` 全部通过
4. 提交 Pull Request，描述变更内容与动机

## 许可证

本项目基于 [MIT License](LICENSE) 开源。

## 相关链接

- [PyPI 项目页面](https://pypi.org/project/py-lmobile-toolkit/)
- [Gitee 代码仓库](https://gitee.com/guolei19850528/py_lmobile_toolkit)
- [乐信官方 API 文档](https://www.lmobile.cn/ApiPages/index.html)
- [英文文档](README.en.md)
