abdds 0.1.0__tar.gz
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.
- abdds-0.1.0/PKG-INFO +193 -0
- abdds-0.1.0/README.md +163 -0
- abdds-0.1.0/pyproject.toml +51 -0
- abdds-0.1.0/setup.cfg +4 -0
- abdds-0.1.0/src/abdds/__init__.py +39 -0
- abdds-0.1.0/src/abdds/_http.py +165 -0
- abdds-0.1.0/src/abdds/client.py +419 -0
- abdds-0.1.0/src/abdds/download.py +125 -0
- abdds-0.1.0/src/abdds/errors.py +38 -0
- abdds-0.1.0/src/abdds/models.py +58 -0
- abdds-0.1.0/src/abdds/py.typed +0 -0
- abdds-0.1.0/src/abdds/upload.py +368 -0
- abdds-0.1.0/src/abdds.egg-info/PKG-INFO +193 -0
- abdds-0.1.0/src/abdds.egg-info/SOURCES.txt +21 -0
- abdds-0.1.0/src/abdds.egg-info/dependency_links.txt +1 -0
- abdds-0.1.0/src/abdds.egg-info/requires.txt +8 -0
- abdds-0.1.0/src/abdds.egg-info/top_level.txt +1 -0
- abdds-0.1.0/tests/test_client.py +254 -0
- abdds-0.1.0/tests/test_download.py +122 -0
- abdds-0.1.0/tests/test_errors.py +70 -0
- abdds-0.1.0/tests/test_http.py +170 -0
- abdds-0.1.0/tests/test_models.py +50 -0
- abdds-0.1.0/tests/test_upload.py +315 -0
abdds-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: abdds
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Async Baidu Disk SDK - 百度网盘异步 Python SDK
|
|
5
|
+
Author: hochenggang
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/hochenggang
|
|
8
|
+
Project-URL: Repository, https://github.com/hochenggang/abdds
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/hochenggang/abdds/issues
|
|
10
|
+
Keywords: baidu,pan,netdisk,sdk,baidupan,baidu-disk,async,aio
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Framework :: AsyncIO
|
|
19
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
Requires-Dist: httpx[http2]>=0.24
|
|
24
|
+
Requires-Dist: aiofiles>=23.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
28
|
+
Requires-Dist: respx>=0.20; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-mock>=3.0; extra == "dev"
|
|
30
|
+
|
|
31
|
+
# abdds
|
|
32
|
+
|
|
33
|
+
Async Baidu Disk SDK - 简单、有效、报错清晰的百度网盘异步 Python SDK。
|
|
34
|
+
|
|
35
|
+
基于 [百度网盘开放平台 API](https://pan.baidu.com/union/doc/) 封装,核心依赖 `httpx` + `aiofiles`,全面异步化。
|
|
36
|
+
|
|
37
|
+
## 安装
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install abdds
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
要求 Python 3.9+。
|
|
44
|
+
|
|
45
|
+
## 快速开始
|
|
46
|
+
|
|
47
|
+
### 1. 获取应用凭证
|
|
48
|
+
|
|
49
|
+
在 [百度网盘开放平台](https://pan.baidu.com/union/doc/) 创建应用,获取 `client_id`、`client_secret` 和 `app_name`。
|
|
50
|
+
|
|
51
|
+
### 2. 初始化客户端
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from pathlib import Path
|
|
55
|
+
from abdds import AsyncBaiduPanClient
|
|
56
|
+
|
|
57
|
+
client = AsyncBaiduPanClient(
|
|
58
|
+
client_id="your_client_id",
|
|
59
|
+
client_secret="your_client_secret",
|
|
60
|
+
app_name="your_app_name",
|
|
61
|
+
)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 3. 授权登录
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
# 首次使用需要授权
|
|
68
|
+
if not client.access_token:
|
|
69
|
+
print(f"请在浏览器打开: {client.auth_url}")
|
|
70
|
+
code = input("请输入授权码: ")
|
|
71
|
+
await client.fetch_token(code)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Token 会自动保存到 `~/.baidupan/` 目录,下次启动无需重新授权。
|
|
75
|
+
|
|
76
|
+
### 4. 使用 API
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
# 查询空间配额
|
|
80
|
+
quota = await client.get_quota()
|
|
81
|
+
print(f"已用: {quota.used / 1024**3:.1f} GB / 总计: {quota.total / 1024**3:.1f} GB")
|
|
82
|
+
|
|
83
|
+
# 上传文件 - 支持 Path / bytes / Generator / AsyncGenerator
|
|
84
|
+
result = await client.upload(Path("local_file.txt"))
|
|
85
|
+
result = await client.upload(Path("data.bin"), file_to=Path("/backup/data.bin"))
|
|
86
|
+
result = await client.upload(b"hello world", file_to=Path("/hello.txt"))
|
|
87
|
+
|
|
88
|
+
# 获取文件元信息
|
|
89
|
+
metas = await client.get_file_metas([result.fs_id])
|
|
90
|
+
dlink = metas[0].dlink
|
|
91
|
+
|
|
92
|
+
# 下载文件 - file_to=None 返回异步流式迭代器,file_to=Path 写入文件
|
|
93
|
+
path = await client.download(dlink, file_to=Path("downloaded.txt"))
|
|
94
|
+
|
|
95
|
+
# 流式下载(适合大文件或自定义处理)
|
|
96
|
+
stream = await client.download(dlink)
|
|
97
|
+
async for chunk in stream:
|
|
98
|
+
process(chunk)
|
|
99
|
+
|
|
100
|
+
# 列出文件
|
|
101
|
+
items = await client.get_file_list("/apps/your_app_name/")
|
|
102
|
+
for item in items:
|
|
103
|
+
print(f"{'[DIR]' if item.is_dir else ' '} {item.filename} ({item.size} bytes)")
|
|
104
|
+
|
|
105
|
+
# 删除文件
|
|
106
|
+
await client.delete_files(["/apps/your_app_name/old_file.txt"])
|
|
107
|
+
|
|
108
|
+
# 使用完毕后关闭
|
|
109
|
+
await client.close()
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
推荐使用异步上下文管理器自动关闭:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
async with AsyncBaiduPanClient(client_id, client_secret, app_name) as client:
|
|
116
|
+
quota = await client.get_quota()
|
|
117
|
+
# ...
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
或使用异步工厂方法:
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
client = await AsyncBaiduPanClient.create(client_id, client_secret, app_name)
|
|
124
|
+
quota = await client.get_quota()
|
|
125
|
+
await client.close()
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## API 参考
|
|
129
|
+
|
|
130
|
+
### AsyncBaiduPanClient
|
|
131
|
+
|
|
132
|
+
| 方法 | 说明 |
|
|
133
|
+
|------|------|
|
|
134
|
+
| `await upload(file_from, file_to=None)` | 异步上传文件。`file_from` 支持 `Path` / `bytes` / `Generator[bytes]` / `AsyncGenerator[bytes]`;`file_to` 为远程路径 `Path`,默认使用文件名 |
|
|
135
|
+
| `await download(dlink, file_to=None, chunk_size=1048576)` | 异步下载文件。`file_to=None` 返回异步字节迭代器;`file_to=Path` 写入本地文件 |
|
|
136
|
+
| `await get_quota()` | 获取空间配额,返回 `ApiQuotaInfo` |
|
|
137
|
+
| `await get_file_metas(fsids)` | 获取文件元信息,返回 `list[ApiFileMeta]` |
|
|
138
|
+
| `await get_file_list(path)` | 列出目录文件,返回 `list[ApiFileListItem]` |
|
|
139
|
+
| `await delete_files(file_paths)` | 批量删除文件 |
|
|
140
|
+
| `await fetch_token(code)` | 通过授权码获取 Token |
|
|
141
|
+
| `await refresh_token()` | 刷新 Token |
|
|
142
|
+
| `await close()` | 关闭客户端 |
|
|
143
|
+
|
|
144
|
+
### 异常体系
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
BaiduPanError
|
|
148
|
+
├── BaiduPanNetworkError # 网络通信异常
|
|
149
|
+
├── BaiduPanAPIError # API 业务异常 (errno, errmsg, request_id)
|
|
150
|
+
└── TokenExpiredError # Token 过期或无效
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
所有异常均继承自 `BaiduPanError`,方便统一捕获。
|
|
154
|
+
|
|
155
|
+
### 特性
|
|
156
|
+
|
|
157
|
+
- **全面异步** — 基于 httpx + aiofiles,原生 async/await
|
|
158
|
+
- **自动重试** — 网络请求自动重试(可配置次数)
|
|
159
|
+
- **Token 管理** — 自动持久化、自动刷新、文件权限限制
|
|
160
|
+
- **分片上传** — 大文件自动分片上传(4MB 分片)
|
|
161
|
+
- **流式下载** — 支持断点续传、指数重试
|
|
162
|
+
- **类型安全** — 完整类型标注,支持 PEP 561 (`py.typed`)
|
|
163
|
+
- **报错清晰** — 异常包含 `errno`、`errmsg`、`request_id`、`url` 等上下文
|
|
164
|
+
|
|
165
|
+
## 与同步版本 bdds 的差异
|
|
166
|
+
|
|
167
|
+
| 特性 | bdds (同步) | abdds (异步) |
|
|
168
|
+
|------|------------|-------------|
|
|
169
|
+
| HTTP 客户端 | requests | httpx |
|
|
170
|
+
| 文件 I/O | 内置 open | aiofiles |
|
|
171
|
+
| 上下文管理器 | `with` / `__enter__` | `async with` / `__aenter__` |
|
|
172
|
+
| 上传数据源 | Path / bytes / Generator | Path / bytes / Generator / AsyncGenerator |
|
|
173
|
+
| 下载返回 | Generator[bytes] | AsyncGenerator[bytes] |
|
|
174
|
+
| Token 回调 | 同步函数 | 异步函数 |
|
|
175
|
+
| 测试 mock | responses | respx |
|
|
176
|
+
|
|
177
|
+
## 开发
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
# 安装开发依赖
|
|
181
|
+
pip install -e ".[dev]"
|
|
182
|
+
|
|
183
|
+
# 运行测试
|
|
184
|
+
python -m pytest -v
|
|
185
|
+
|
|
186
|
+
# 构建
|
|
187
|
+
pip install build twine
|
|
188
|
+
python -m build
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## License
|
|
192
|
+
|
|
193
|
+
MIT
|
abdds-0.1.0/README.md
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# abdds
|
|
2
|
+
|
|
3
|
+
Async Baidu Disk SDK - 简单、有效、报错清晰的百度网盘异步 Python SDK。
|
|
4
|
+
|
|
5
|
+
基于 [百度网盘开放平台 API](https://pan.baidu.com/union/doc/) 封装,核心依赖 `httpx` + `aiofiles`,全面异步化。
|
|
6
|
+
|
|
7
|
+
## 安装
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install abdds
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
要求 Python 3.9+。
|
|
14
|
+
|
|
15
|
+
## 快速开始
|
|
16
|
+
|
|
17
|
+
### 1. 获取应用凭证
|
|
18
|
+
|
|
19
|
+
在 [百度网盘开放平台](https://pan.baidu.com/union/doc/) 创建应用,获取 `client_id`、`client_secret` 和 `app_name`。
|
|
20
|
+
|
|
21
|
+
### 2. 初始化客户端
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from abdds import AsyncBaiduPanClient
|
|
26
|
+
|
|
27
|
+
client = AsyncBaiduPanClient(
|
|
28
|
+
client_id="your_client_id",
|
|
29
|
+
client_secret="your_client_secret",
|
|
30
|
+
app_name="your_app_name",
|
|
31
|
+
)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### 3. 授权登录
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
# 首次使用需要授权
|
|
38
|
+
if not client.access_token:
|
|
39
|
+
print(f"请在浏览器打开: {client.auth_url}")
|
|
40
|
+
code = input("请输入授权码: ")
|
|
41
|
+
await client.fetch_token(code)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Token 会自动保存到 `~/.baidupan/` 目录,下次启动无需重新授权。
|
|
45
|
+
|
|
46
|
+
### 4. 使用 API
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
# 查询空间配额
|
|
50
|
+
quota = await client.get_quota()
|
|
51
|
+
print(f"已用: {quota.used / 1024**3:.1f} GB / 总计: {quota.total / 1024**3:.1f} GB")
|
|
52
|
+
|
|
53
|
+
# 上传文件 - 支持 Path / bytes / Generator / AsyncGenerator
|
|
54
|
+
result = await client.upload(Path("local_file.txt"))
|
|
55
|
+
result = await client.upload(Path("data.bin"), file_to=Path("/backup/data.bin"))
|
|
56
|
+
result = await client.upload(b"hello world", file_to=Path("/hello.txt"))
|
|
57
|
+
|
|
58
|
+
# 获取文件元信息
|
|
59
|
+
metas = await client.get_file_metas([result.fs_id])
|
|
60
|
+
dlink = metas[0].dlink
|
|
61
|
+
|
|
62
|
+
# 下载文件 - file_to=None 返回异步流式迭代器,file_to=Path 写入文件
|
|
63
|
+
path = await client.download(dlink, file_to=Path("downloaded.txt"))
|
|
64
|
+
|
|
65
|
+
# 流式下载(适合大文件或自定义处理)
|
|
66
|
+
stream = await client.download(dlink)
|
|
67
|
+
async for chunk in stream:
|
|
68
|
+
process(chunk)
|
|
69
|
+
|
|
70
|
+
# 列出文件
|
|
71
|
+
items = await client.get_file_list("/apps/your_app_name/")
|
|
72
|
+
for item in items:
|
|
73
|
+
print(f"{'[DIR]' if item.is_dir else ' '} {item.filename} ({item.size} bytes)")
|
|
74
|
+
|
|
75
|
+
# 删除文件
|
|
76
|
+
await client.delete_files(["/apps/your_app_name/old_file.txt"])
|
|
77
|
+
|
|
78
|
+
# 使用完毕后关闭
|
|
79
|
+
await client.close()
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
推荐使用异步上下文管理器自动关闭:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
async with AsyncBaiduPanClient(client_id, client_secret, app_name) as client:
|
|
86
|
+
quota = await client.get_quota()
|
|
87
|
+
# ...
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
或使用异步工厂方法:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
client = await AsyncBaiduPanClient.create(client_id, client_secret, app_name)
|
|
94
|
+
quota = await client.get_quota()
|
|
95
|
+
await client.close()
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## API 参考
|
|
99
|
+
|
|
100
|
+
### AsyncBaiduPanClient
|
|
101
|
+
|
|
102
|
+
| 方法 | 说明 |
|
|
103
|
+
|------|------|
|
|
104
|
+
| `await upload(file_from, file_to=None)` | 异步上传文件。`file_from` 支持 `Path` / `bytes` / `Generator[bytes]` / `AsyncGenerator[bytes]`;`file_to` 为远程路径 `Path`,默认使用文件名 |
|
|
105
|
+
| `await download(dlink, file_to=None, chunk_size=1048576)` | 异步下载文件。`file_to=None` 返回异步字节迭代器;`file_to=Path` 写入本地文件 |
|
|
106
|
+
| `await get_quota()` | 获取空间配额,返回 `ApiQuotaInfo` |
|
|
107
|
+
| `await get_file_metas(fsids)` | 获取文件元信息,返回 `list[ApiFileMeta]` |
|
|
108
|
+
| `await get_file_list(path)` | 列出目录文件,返回 `list[ApiFileListItem]` |
|
|
109
|
+
| `await delete_files(file_paths)` | 批量删除文件 |
|
|
110
|
+
| `await fetch_token(code)` | 通过授权码获取 Token |
|
|
111
|
+
| `await refresh_token()` | 刷新 Token |
|
|
112
|
+
| `await close()` | 关闭客户端 |
|
|
113
|
+
|
|
114
|
+
### 异常体系
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
BaiduPanError
|
|
118
|
+
├── BaiduPanNetworkError # 网络通信异常
|
|
119
|
+
├── BaiduPanAPIError # API 业务异常 (errno, errmsg, request_id)
|
|
120
|
+
└── TokenExpiredError # Token 过期或无效
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
所有异常均继承自 `BaiduPanError`,方便统一捕获。
|
|
124
|
+
|
|
125
|
+
### 特性
|
|
126
|
+
|
|
127
|
+
- **全面异步** — 基于 httpx + aiofiles,原生 async/await
|
|
128
|
+
- **自动重试** — 网络请求自动重试(可配置次数)
|
|
129
|
+
- **Token 管理** — 自动持久化、自动刷新、文件权限限制
|
|
130
|
+
- **分片上传** — 大文件自动分片上传(4MB 分片)
|
|
131
|
+
- **流式下载** — 支持断点续传、指数重试
|
|
132
|
+
- **类型安全** — 完整类型标注,支持 PEP 561 (`py.typed`)
|
|
133
|
+
- **报错清晰** — 异常包含 `errno`、`errmsg`、`request_id`、`url` 等上下文
|
|
134
|
+
|
|
135
|
+
## 与同步版本 bdds 的差异
|
|
136
|
+
|
|
137
|
+
| 特性 | bdds (同步) | abdds (异步) |
|
|
138
|
+
|------|------------|-------------|
|
|
139
|
+
| HTTP 客户端 | requests | httpx |
|
|
140
|
+
| 文件 I/O | 内置 open | aiofiles |
|
|
141
|
+
| 上下文管理器 | `with` / `__enter__` | `async with` / `__aenter__` |
|
|
142
|
+
| 上传数据源 | Path / bytes / Generator | Path / bytes / Generator / AsyncGenerator |
|
|
143
|
+
| 下载返回 | Generator[bytes] | AsyncGenerator[bytes] |
|
|
144
|
+
| Token 回调 | 同步函数 | 异步函数 |
|
|
145
|
+
| 测试 mock | responses | respx |
|
|
146
|
+
|
|
147
|
+
## 开发
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
# 安装开发依赖
|
|
151
|
+
pip install -e ".[dev]"
|
|
152
|
+
|
|
153
|
+
# 运行测试
|
|
154
|
+
python -m pytest -v
|
|
155
|
+
|
|
156
|
+
# 构建
|
|
157
|
+
pip install build twine
|
|
158
|
+
python -m build
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## License
|
|
162
|
+
|
|
163
|
+
MIT
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "abdds"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Async Baidu Disk SDK - 百度网盘异步 Python SDK"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "hochenggang" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["baidu", "pan", "netdisk", "sdk", "baidupan", "baidu-disk", "async", "aio"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.9",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Framework :: AsyncIO",
|
|
25
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
26
|
+
"Typing :: Typed",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"httpx[http2]>=0.24",
|
|
30
|
+
"aiofiles>=23.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
dev = [
|
|
35
|
+
"pytest>=7.0",
|
|
36
|
+
"pytest-asyncio>=0.21",
|
|
37
|
+
"respx>=0.20",
|
|
38
|
+
"pytest-mock>=3.0",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[project.urls]
|
|
42
|
+
Homepage = "https://github.com/hochenggang"
|
|
43
|
+
Repository = "https://github.com/hochenggang/abdds"
|
|
44
|
+
"Bug Tracker" = "https://github.com/hochenggang/abdds/issues"
|
|
45
|
+
|
|
46
|
+
[tool.setuptools.packages.find]
|
|
47
|
+
where = ["src"]
|
|
48
|
+
|
|
49
|
+
[tool.pytest.ini_options]
|
|
50
|
+
testpaths = ["tests"]
|
|
51
|
+
asyncio_mode = "auto"
|
abdds-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""abdds - 百度网盘异步 Python SDK (async-baidu-disk-sdk)
|
|
2
|
+
|
|
3
|
+
简单、有效、报错清晰的百度网盘异步 SDK。
|
|
4
|
+
|
|
5
|
+
Usage::
|
|
6
|
+
|
|
7
|
+
from abdds import AsyncBaiduPanClient
|
|
8
|
+
|
|
9
|
+
async with AsyncBaiduPanClient(client_id, client_secret, app_name) as client:
|
|
10
|
+
quota = await client.get_quota()
|
|
11
|
+
await client.upload(Path("file.txt"), file_to=Path("/docs/file.txt"))
|
|
12
|
+
await client.download(dlink, file_to=Path("output.txt"))
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
__version__ = "0.1.0"
|
|
16
|
+
|
|
17
|
+
from .client import AsyncBaiduPanClient
|
|
18
|
+
from .errors import (
|
|
19
|
+
BaiduPanAPIError,
|
|
20
|
+
BaiduPanError,
|
|
21
|
+
BaiduPanNetworkError,
|
|
22
|
+
TokenExpiredError,
|
|
23
|
+
)
|
|
24
|
+
from .models import ApiFileListItem, ApiFileMeta, ApiQuotaInfo, UploadResult
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
# Client
|
|
28
|
+
"AsyncBaiduPanClient",
|
|
29
|
+
# Errors
|
|
30
|
+
"BaiduPanError",
|
|
31
|
+
"BaiduPanNetworkError",
|
|
32
|
+
"BaiduPanAPIError",
|
|
33
|
+
"TokenExpiredError",
|
|
34
|
+
# Models
|
|
35
|
+
"UploadResult",
|
|
36
|
+
"ApiFileMeta",
|
|
37
|
+
"ApiFileListItem",
|
|
38
|
+
"ApiQuotaInfo",
|
|
39
|
+
]
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""HTTP 传输层 - 异步 Session 管理、请求封装、重试策略"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Any, Awaitable, Callable
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from .errors import BaiduPanAPIError, BaiduPanNetworkError, TokenExpiredError
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("abdds")
|
|
14
|
+
|
|
15
|
+
UserAgent = "netdisk;2.2.51.6;netdisk;10.0;PC;PC;Mac OS X 10.15.7;en-US"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _truncate_data(data: Any, max_len: int = 500) -> Any:
|
|
19
|
+
"""递归截断超长字符串和二进制数据,用于日志安全输出"""
|
|
20
|
+
if isinstance(data, dict):
|
|
21
|
+
return {k: _truncate_data(v, max_len) for k, v in data.items()}
|
|
22
|
+
if isinstance(data, (list, tuple, set)):
|
|
23
|
+
return [_truncate_data(i, max_len) for i in data]
|
|
24
|
+
if isinstance(data, (bytes, bytearray)):
|
|
25
|
+
return f"<Binary Data: {len(data)} bytes>"
|
|
26
|
+
if isinstance(data, str) and len(data) > max_len:
|
|
27
|
+
return f"{data[:max_len]}... [truncated]"
|
|
28
|
+
return data
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class _HttpTransport:
|
|
32
|
+
"""封装 httpx.AsyncClient,提供自动 Token 注入、重试和错误处理"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
max_retries: int = 3,
|
|
37
|
+
timeout: tuple[int, int] = (100, 600),
|
|
38
|
+
) -> None:
|
|
39
|
+
self._max_retries = max_retries
|
|
40
|
+
self._timeout = timeout
|
|
41
|
+
self._client: httpx.AsyncClient | None = None
|
|
42
|
+
self._closed: bool = False
|
|
43
|
+
self._access_token: str | None = None
|
|
44
|
+
self._on_token_expired: Callable[[], Awaitable[str | None]] | None = None
|
|
45
|
+
|
|
46
|
+
def _init_client(self) -> httpx.AsyncClient:
|
|
47
|
+
transport = httpx.AsyncHTTPTransport(
|
|
48
|
+
retries=self._max_retries,
|
|
49
|
+
)
|
|
50
|
+
client = httpx.AsyncClient(
|
|
51
|
+
transport=transport,
|
|
52
|
+
timeout=httpx.Timeout(self._timeout[1], connect=self._timeout[0]),
|
|
53
|
+
headers={"User-Agent": UserAgent},
|
|
54
|
+
follow_redirects=True,
|
|
55
|
+
)
|
|
56
|
+
return client
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def client(self) -> httpx.AsyncClient:
|
|
60
|
+
"""获取或初始化 AsyncClient"""
|
|
61
|
+
if self._closed:
|
|
62
|
+
raise RuntimeError("Transport is closed, cannot make requests")
|
|
63
|
+
if self._client is None:
|
|
64
|
+
self._client = self._init_client()
|
|
65
|
+
return self._client
|
|
66
|
+
|
|
67
|
+
def set_access_token(self, token: str | None) -> None:
|
|
68
|
+
self._access_token = token
|
|
69
|
+
|
|
70
|
+
def set_token_expired_callback(
|
|
71
|
+
self, callback: Callable[[], Awaitable[str | None]]
|
|
72
|
+
) -> None:
|
|
73
|
+
"""设置 Token 过期时的异步刷新回调,回调应返回新的 access_token"""
|
|
74
|
+
self._on_token_expired = callback
|
|
75
|
+
|
|
76
|
+
async def request(
|
|
77
|
+
self, method: str, url: str, **kwargs: Any
|
|
78
|
+
) -> dict | httpx.Response:
|
|
79
|
+
"""
|
|
80
|
+
核心异步请求方法:
|
|
81
|
+
1. 自动注入 access_token
|
|
82
|
+
2. 处理网络层错误 (由 httpx transport retries 处理重试)
|
|
83
|
+
3. 处理业务层错误 (解析 errno)
|
|
84
|
+
4. Token 失效时自动刷新并重试一次
|
|
85
|
+
"""
|
|
86
|
+
logger.debug("method:%s url:%s kwargs:%s", method, url, _truncate_data(kwargs))
|
|
87
|
+
|
|
88
|
+
if self._closed:
|
|
89
|
+
raise RuntimeError("Transport is closed, cannot make requests")
|
|
90
|
+
|
|
91
|
+
if not self._access_token:
|
|
92
|
+
raise TokenExpiredError("Access token not initialized, please login first")
|
|
93
|
+
|
|
94
|
+
params = kwargs.pop("params", {})
|
|
95
|
+
params["access_token"] = self._access_token
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
response = await self.client.request(method, url, params=params, **kwargs)
|
|
99
|
+
response.raise_for_status()
|
|
100
|
+
|
|
101
|
+
# 流式响应直接返回
|
|
102
|
+
if kwargs.get("stream"):
|
|
103
|
+
return response # type: ignore[return-value]
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
data = response.json()
|
|
107
|
+
except (json.JSONDecodeError, ValueError):
|
|
108
|
+
raise BaiduPanAPIError(-1, "Invalid JSON response")
|
|
109
|
+
logger.debug("method:%s url:%s response:%s", method, url, _truncate_data(data))
|
|
110
|
+
|
|
111
|
+
except httpx.HTTPStatusError as e:
|
|
112
|
+
raise BaiduPanNetworkError(
|
|
113
|
+
f"HTTP {e.response.status_code}: {e}", url=url,
|
|
114
|
+
status_code=e.response.status_code,
|
|
115
|
+
) from e
|
|
116
|
+
except httpx.RequestError as e:
|
|
117
|
+
raise BaiduPanNetworkError(
|
|
118
|
+
f"Request failed: {e}", url=url
|
|
119
|
+
) from e
|
|
120
|
+
|
|
121
|
+
# 业务错误检查
|
|
122
|
+
errno = data.get("errno", 0)
|
|
123
|
+
if errno != 0:
|
|
124
|
+
# Token 失效 (errno: -6 或 11)
|
|
125
|
+
if errno in (-6, 11) and self._on_token_expired:
|
|
126
|
+
logger.warning("Token expired during request, refreshing...")
|
|
127
|
+
new_token = await self._on_token_expired()
|
|
128
|
+
if new_token:
|
|
129
|
+
params["access_token"] = new_token
|
|
130
|
+
try:
|
|
131
|
+
response = await self.client.request(
|
|
132
|
+
method, url, params=params, **kwargs
|
|
133
|
+
)
|
|
134
|
+
response.raise_for_status()
|
|
135
|
+
data = response.json()
|
|
136
|
+
except httpx.RequestError as e:
|
|
137
|
+
raise BaiduPanNetworkError(
|
|
138
|
+
f"Retry after token refresh failed: {e}", url=url
|
|
139
|
+
) from e
|
|
140
|
+
except (json.JSONDecodeError, ValueError):
|
|
141
|
+
raise BaiduPanAPIError(-1, "Invalid JSON response")
|
|
142
|
+
|
|
143
|
+
if data.get("errno", 0) == 0:
|
|
144
|
+
return data
|
|
145
|
+
|
|
146
|
+
# 刷新后仍失败
|
|
147
|
+
raise BaiduPanAPIError(
|
|
148
|
+
data.get("errno", errno),
|
|
149
|
+
data.get("errmsg", "Token refresh failed"),
|
|
150
|
+
request_id=data.get("request_id", ""),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
raise BaiduPanAPIError(
|
|
154
|
+
errno,
|
|
155
|
+
data.get("errmsg", "Unknown error"),
|
|
156
|
+
request_id=data.get("request_id", ""),
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
return data
|
|
160
|
+
|
|
161
|
+
async def close(self) -> None:
|
|
162
|
+
self._closed = True
|
|
163
|
+
if self._client is not None:
|
|
164
|
+
await self._client.aclose()
|
|
165
|
+
self._client = None
|