bdds 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.
bdds-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 bdds contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
bdds-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,168 @@
1
+ Metadata-Version: 2.4
2
+ Name: bdds
3
+ Version: 0.1.0
4
+ Summary: 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/bdds
9
+ Project-URL: Bug Tracker, https://github.com/hochenggang/bdds/issues
10
+ Keywords: baidu,pan,netdisk,sdk,baidupan,baidu-disk
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: Topic :: Internet :: WWW/HTTP
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: requests>=2.20
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=7.0; extra == "dev"
26
+ Requires-Dist: pytest-mock>=3.0; extra == "dev"
27
+ Requires-Dist: responses>=0.23; extra == "dev"
28
+ Dynamic: license-file
29
+
30
+ # bdds
31
+
32
+ Baidu Disk SDK - 简单、有效、报错清晰的百度网盘 Python SDK。
33
+
34
+ 基于 [百度网盘开放平台 API](https://pan.baidu.com/union/doc/) 封装,核心依赖仅 `requests`。
35
+
36
+ ## 安装
37
+
38
+ ```bash
39
+ pip install bdds
40
+ ```
41
+
42
+ ## 快速开始
43
+
44
+ ### 1. 获取应用凭证
45
+
46
+ 在 [百度网盘开放平台](https://pan.baidu.com/union/doc/) 创建应用,获取 `client_id`、`client_secret` 和 `app_name`。
47
+
48
+ ### 2. 初始化客户端
49
+
50
+ ```python
51
+ from pathlib import Path
52
+ from bdds import BaiduPanClient
53
+
54
+ client = BaiduPanClient(
55
+ client_id="your_client_id",
56
+ client_secret="your_client_secret",
57
+ app_name="your_app_name",
58
+ )
59
+ ```
60
+
61
+ ### 3. 授权登录
62
+
63
+ ```python
64
+ # 首次使用需要授权
65
+ if not client.access_token:
66
+ print(f"请在浏览器打开: {client.auth_url}")
67
+ code = input("请输入授权码: ")
68
+ client.fetch_token(code)
69
+ ```
70
+
71
+ Token 会自动保存到 `~/.baidupan/` 目录,下次启动无需重新授权。
72
+
73
+ ### 4. 使用 API
74
+
75
+ ```python
76
+ # 查询空间配额
77
+ quota = client.get_quota()
78
+ print(f"已用: {quota.used / 1024**3:.1f} GB / 总计: {quota.total / 1024**3:.1f} GB")
79
+
80
+ # 上传文件 - 支持 Path / bytes / Generator
81
+ result = client.upload(Path("local_file.txt"))
82
+ result = client.upload(Path("data.bin"), file_to=Path("/backup/data.bin"))
83
+ result = client.upload(b"hello world", file_to=Path("/hello.txt"))
84
+
85
+ # 获取文件元信息
86
+ metas = client.get_file_metas([result.fs_id])
87
+ dlink = metas[0].dlink
88
+
89
+ # 下载文件 - file_to=None 返回流式迭代器,file_to=Path 写入文件
90
+ path = client.download(dlink, file_to=Path("downloaded.txt"))
91
+
92
+ # 流式下载(适合大文件或自定义处理)
93
+ for chunk in client.download(dlink):
94
+ process(chunk)
95
+
96
+ # 列出文件
97
+ items = client.get_file_list("/apps/your_app_name/")
98
+ for item in items:
99
+ print(f"{'[DIR]' if item.is_dir else ' '} {item.filename} ({item.size} bytes)")
100
+
101
+ # 删除文件
102
+ client.delete_files(["/apps/your_app_name/old_file.txt"])
103
+
104
+ # 使用完毕后关闭
105
+ client.close()
106
+ ```
107
+
108
+ 推荐使用上下文管理器自动关闭:
109
+
110
+ ```python
111
+ with BaiduPanClient(client_id, client_secret, app_name) as client:
112
+ quota = client.get_quota()
113
+ # ...
114
+ ```
115
+
116
+ ## API 参考
117
+
118
+ ### BaiduPanClient
119
+
120
+ | 方法 | 说明 |
121
+ |------|------|
122
+ | `upload(file_from, file_to=None)` | 上传文件。`file_from` 支持 `Path` / `bytes` / `Generator[bytes]`;`file_to` 为远程路径 `Path`,默认使用文件名 |
123
+ | `download(dlink, file_to=None, chunk_size=1048576)` | 下载文件。`file_to=None` 返回字节迭代器;`file_to=Path` 写入本地文件 |
124
+ | `get_quota()` | 获取空间配额,返回 `ApiQuotaInfo` |
125
+ | `get_file_metas(fsids)` | 获取文件元信息,返回 `list[ApiFileMeta]` |
126
+ | `get_file_list(path)` | 列出目录文件,返回 `list[ApiFileListItem]` |
127
+ | `delete_files(file_paths)` | 批量删除文件 |
128
+ | `fetch_token(code)` | 通过授权码获取 Token |
129
+ | `refresh_token()` | 刷新 Token |
130
+ | `close()` | 关闭客户端 |
131
+
132
+ ### 异常体系
133
+
134
+ ```
135
+ BaiduPanError
136
+ ├── BaiduPanNetworkError # 网络通信异常
137
+ ├── BaiduPanAPIError # API 业务异常 (errno, errmsg, request_id)
138
+ └── TokenExpiredError # Token 过期或无效
139
+ ```
140
+
141
+ 所有异常均继承自 `BaiduPanError`,方便统一捕获。
142
+
143
+ ### 特性
144
+
145
+ - **自动重试** — 网络请求自动重试(可配置次数),支持指数退避
146
+ - **Token 管理** — 自动持久化、自动刷新、文件权限限制
147
+ - **分片上传** — 大文件自动分片上传(4MB 分片)
148
+ - **流式下载** — 支持断点续传、指数重试
149
+ - **类型安全** — 完整类型标注,支持 PEP 561 (`py.typed`)
150
+ - **报错清晰** — 异常包含 `errno`、`errmsg`、`request_id`、`url` 等上下文
151
+
152
+ ## 开发
153
+
154
+ ```bash
155
+ # 安装开发依赖
156
+ pip install -e ".[dev]"
157
+
158
+ # 运行测试
159
+ python -m pytest -v
160
+
161
+ # 构建
162
+ pip install build twine
163
+ python -m build
164
+ ```
165
+
166
+ ## License
167
+
168
+ MIT
bdds-0.1.0/README.md ADDED
@@ -0,0 +1,139 @@
1
+ # bdds
2
+
3
+ Baidu Disk SDK - 简单、有效、报错清晰的百度网盘 Python SDK。
4
+
5
+ 基于 [百度网盘开放平台 API](https://pan.baidu.com/union/doc/) 封装,核心依赖仅 `requests`。
6
+
7
+ ## 安装
8
+
9
+ ```bash
10
+ pip install bdds
11
+ ```
12
+
13
+ ## 快速开始
14
+
15
+ ### 1. 获取应用凭证
16
+
17
+ 在 [百度网盘开放平台](https://pan.baidu.com/union/doc/) 创建应用,获取 `client_id`、`client_secret` 和 `app_name`。
18
+
19
+ ### 2. 初始化客户端
20
+
21
+ ```python
22
+ from pathlib import Path
23
+ from bdds import BaiduPanClient
24
+
25
+ client = BaiduPanClient(
26
+ client_id="your_client_id",
27
+ client_secret="your_client_secret",
28
+ app_name="your_app_name",
29
+ )
30
+ ```
31
+
32
+ ### 3. 授权登录
33
+
34
+ ```python
35
+ # 首次使用需要授权
36
+ if not client.access_token:
37
+ print(f"请在浏览器打开: {client.auth_url}")
38
+ code = input("请输入授权码: ")
39
+ client.fetch_token(code)
40
+ ```
41
+
42
+ Token 会自动保存到 `~/.baidupan/` 目录,下次启动无需重新授权。
43
+
44
+ ### 4. 使用 API
45
+
46
+ ```python
47
+ # 查询空间配额
48
+ quota = client.get_quota()
49
+ print(f"已用: {quota.used / 1024**3:.1f} GB / 总计: {quota.total / 1024**3:.1f} GB")
50
+
51
+ # 上传文件 - 支持 Path / bytes / Generator
52
+ result = client.upload(Path("local_file.txt"))
53
+ result = client.upload(Path("data.bin"), file_to=Path("/backup/data.bin"))
54
+ result = client.upload(b"hello world", file_to=Path("/hello.txt"))
55
+
56
+ # 获取文件元信息
57
+ metas = client.get_file_metas([result.fs_id])
58
+ dlink = metas[0].dlink
59
+
60
+ # 下载文件 - file_to=None 返回流式迭代器,file_to=Path 写入文件
61
+ path = client.download(dlink, file_to=Path("downloaded.txt"))
62
+
63
+ # 流式下载(适合大文件或自定义处理)
64
+ for chunk in client.download(dlink):
65
+ process(chunk)
66
+
67
+ # 列出文件
68
+ items = client.get_file_list("/apps/your_app_name/")
69
+ for item in items:
70
+ print(f"{'[DIR]' if item.is_dir else ' '} {item.filename} ({item.size} bytes)")
71
+
72
+ # 删除文件
73
+ client.delete_files(["/apps/your_app_name/old_file.txt"])
74
+
75
+ # 使用完毕后关闭
76
+ client.close()
77
+ ```
78
+
79
+ 推荐使用上下文管理器自动关闭:
80
+
81
+ ```python
82
+ with BaiduPanClient(client_id, client_secret, app_name) as client:
83
+ quota = client.get_quota()
84
+ # ...
85
+ ```
86
+
87
+ ## API 参考
88
+
89
+ ### BaiduPanClient
90
+
91
+ | 方法 | 说明 |
92
+ |------|------|
93
+ | `upload(file_from, file_to=None)` | 上传文件。`file_from` 支持 `Path` / `bytes` / `Generator[bytes]`;`file_to` 为远程路径 `Path`,默认使用文件名 |
94
+ | `download(dlink, file_to=None, chunk_size=1048576)` | 下载文件。`file_to=None` 返回字节迭代器;`file_to=Path` 写入本地文件 |
95
+ | `get_quota()` | 获取空间配额,返回 `ApiQuotaInfo` |
96
+ | `get_file_metas(fsids)` | 获取文件元信息,返回 `list[ApiFileMeta]` |
97
+ | `get_file_list(path)` | 列出目录文件,返回 `list[ApiFileListItem]` |
98
+ | `delete_files(file_paths)` | 批量删除文件 |
99
+ | `fetch_token(code)` | 通过授权码获取 Token |
100
+ | `refresh_token()` | 刷新 Token |
101
+ | `close()` | 关闭客户端 |
102
+
103
+ ### 异常体系
104
+
105
+ ```
106
+ BaiduPanError
107
+ ├── BaiduPanNetworkError # 网络通信异常
108
+ ├── BaiduPanAPIError # API 业务异常 (errno, errmsg, request_id)
109
+ └── TokenExpiredError # Token 过期或无效
110
+ ```
111
+
112
+ 所有异常均继承自 `BaiduPanError`,方便统一捕获。
113
+
114
+ ### 特性
115
+
116
+ - **自动重试** — 网络请求自动重试(可配置次数),支持指数退避
117
+ - **Token 管理** — 自动持久化、自动刷新、文件权限限制
118
+ - **分片上传** — 大文件自动分片上传(4MB 分片)
119
+ - **流式下载** — 支持断点续传、指数重试
120
+ - **类型安全** — 完整类型标注,支持 PEP 561 (`py.typed`)
121
+ - **报错清晰** — 异常包含 `errno`、`errmsg`、`request_id`、`url` 等上下文
122
+
123
+ ## 开发
124
+
125
+ ```bash
126
+ # 安装开发依赖
127
+ pip install -e ".[dev]"
128
+
129
+ # 运行测试
130
+ python -m pytest -v
131
+
132
+ # 构建
133
+ pip install build twine
134
+ python -m build
135
+ ```
136
+
137
+ ## License
138
+
139
+ MIT
@@ -0,0 +1,47 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "bdds"
7
+ version = "0.1.0"
8
+ description = "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"]
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
+ "Topic :: Internet :: WWW/HTTP",
25
+ "Typing :: Typed",
26
+ ]
27
+ dependencies = [
28
+ "requests>=2.20",
29
+ ]
30
+
31
+ [project.optional-dependencies]
32
+ dev = [
33
+ "pytest>=7.0",
34
+ "pytest-mock>=3.0",
35
+ "responses>=0.23",
36
+ ]
37
+
38
+ [project.urls]
39
+ Homepage = "https://github.com/hochenggang"
40
+ Repository = "https://github.com/hochenggang/bdds"
41
+ "Bug Tracker" = "https://github.com/hochenggang/bdds/issues"
42
+
43
+ [tool.setuptools.packages.find]
44
+ where = ["src"]
45
+
46
+ [tool.pytest.ini_options]
47
+ testpaths = ["tests"]
bdds-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,39 @@
1
+ """bdds - 百度网盘 Python SDK (baidu-disk-sdk)
2
+
3
+ 简单、有效、报错清晰的百度网盘同步 SDK。
4
+
5
+ Usage::
6
+
7
+ from bdds import BaiduPanClient
8
+
9
+ with BaiduPanClient(client_id, client_secret, app_name) as client:
10
+ quota = client.get_quota()
11
+ client.upload(Path("file.txt"), file_to=Path("/docs/file.txt"))
12
+ client.download(dlink, file_to=Path("output.txt"))
13
+ """
14
+
15
+ __version__ = "0.1.0"
16
+
17
+ from .client import BaiduPanClient
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
+ "BaiduPanClient",
29
+ # Errors
30
+ "BaiduPanError",
31
+ "BaiduPanNetworkError",
32
+ "BaiduPanAPIError",
33
+ "TokenExpiredError",
34
+ # Models
35
+ "UploadResult",
36
+ "ApiFileMeta",
37
+ "ApiFileListItem",
38
+ "ApiQuotaInfo",
39
+ ]
@@ -0,0 +1,156 @@
1
+ """HTTP 传输层 - Session 管理、请求封装、重试策略"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ from typing import Any, Callable
8
+
9
+ import requests
10
+ from requests.adapters import HTTPAdapter
11
+ from urllib3.util.retry import Retry
12
+
13
+ from .errors import BaiduPanAPIError, BaiduPanNetworkError, TokenExpiredError
14
+
15
+ logger = logging.getLogger("bdds")
16
+
17
+ UserAgent = "netdisk;2.2.51.6;netdisk;10.0;PC;PC;Mac OS X 10.15.7;en-US"
18
+
19
+
20
+ def _truncate_data(data: Any, max_len: int = 500) -> Any:
21
+ """递归截断超长字符串和二进制数据,用于日志安全输出"""
22
+ if isinstance(data, dict):
23
+ return {k: _truncate_data(v, max_len) for k, v in data.items()}
24
+ if isinstance(data, (list, tuple, set)):
25
+ return [_truncate_data(i, max_len) for i in data]
26
+ if isinstance(data, (bytes, bytearray)):
27
+ return f"<Binary Data: {len(data)} bytes>"
28
+ if isinstance(data, str) and len(data) > max_len:
29
+ return f"{data[:max_len]}... [truncated]"
30
+ return data
31
+
32
+
33
+ class _HttpTransport:
34
+ """封装 requests.Session,提供自动 Token 注入、重试和错误处理"""
35
+
36
+ def __init__(
37
+ self,
38
+ max_retries: int = 3,
39
+ timeout: tuple[int, int] = (100, 600),
40
+ ) -> None:
41
+ self._max_retries = max_retries
42
+ self._timeout = timeout
43
+ self._session = self._init_session()
44
+ self._access_token: str | None = None
45
+ self._on_token_expired: Callable[[], str | None] | None = None
46
+
47
+ def _init_session(self) -> requests.Session:
48
+ session = requests.Session()
49
+ retries = Retry(
50
+ total=self._max_retries,
51
+ backoff_factor=1,
52
+ status_forcelist=[500, 502, 503, 504],
53
+ allowed_methods=["GET", "POST", "PUT", "DELETE"],
54
+ raise_on_status=True,
55
+ )
56
+ adapter = HTTPAdapter(max_retries=retries)
57
+ session.mount("http://", adapter)
58
+ session.mount("https://", adapter)
59
+ session.headers.update({"User-Agent": UserAgent})
60
+ return session
61
+
62
+ @property
63
+ def session(self) -> requests.Session:
64
+ return self._session
65
+
66
+ def set_access_token(self, token: str | None) -> None:
67
+ self._access_token = token
68
+
69
+ def set_token_expired_callback(self, callback: Callable[[], str | None]) -> None:
70
+ """设置 Token 过期时的刷新回调,回调应返回新的 access_token"""
71
+ self._on_token_expired = callback
72
+
73
+ def request(self, method: str, url: str, **kwargs: Any) -> dict | requests.Response:
74
+ """
75
+ 核心请求方法:
76
+ 1. 自动注入 access_token
77
+ 2. 处理网络层错误 (由 Session Retry 处理重试)
78
+ 3. 处理业务层错误 (解析 errno)
79
+ 4. Token 失效时自动刷新并重试一次
80
+ """
81
+ logger.debug("method:%s url:%s kwargs:%s", method, url, _truncate_data(kwargs))
82
+
83
+ if self._session is None:
84
+ raise RuntimeError("Transport is closed, cannot make requests")
85
+
86
+ if not self._access_token:
87
+ raise TokenExpiredError("Access token not initialized, please login first")
88
+
89
+ params = kwargs.pop("params", {})
90
+ params["access_token"] = self._access_token
91
+
92
+ kwargs.setdefault("timeout", self._timeout)
93
+
94
+ try:
95
+ response = self._session.request(method, url, params=params, **kwargs)
96
+ response.raise_for_status()
97
+
98
+ # 流式响应直接返回
99
+ if kwargs.get("stream"):
100
+ return response # type: ignore[return-value]
101
+
102
+ try:
103
+ data = response.json()
104
+ except (json.JSONDecodeError, ValueError):
105
+ raise BaiduPanAPIError(-1, "Invalid JSON response")
106
+ logger.debug("method:%s url:%s response:%s", method, url, _truncate_data(data))
107
+
108
+ except requests.RequestException as e:
109
+ raise BaiduPanNetworkError(
110
+ f"Request failed: {e}", url=url
111
+ ) from e
112
+
113
+ # 业务错误检查
114
+ errno = data.get("errno", 0)
115
+ if errno != 0:
116
+ # Token 失效 (errno: -6 或 11)
117
+ if errno in (-6, 11) and self._on_token_expired:
118
+ logger.warning("Token expired during request, refreshing...")
119
+ new_token = self._on_token_expired()
120
+ if new_token:
121
+ params["access_token"] = new_token
122
+ try:
123
+ response = self._session.request(
124
+ method, url, params=params, **kwargs
125
+ )
126
+ response.raise_for_status()
127
+ data = response.json()
128
+ except requests.RequestException as e:
129
+ raise BaiduPanNetworkError(
130
+ f"Retry after token refresh failed: {e}", url=url
131
+ ) from e
132
+ except (json.JSONDecodeError, ValueError):
133
+ raise BaiduPanAPIError(-1, "Invalid JSON response")
134
+
135
+ if data.get("errno", 0) == 0:
136
+ return data
137
+
138
+ # 刷新后仍失败
139
+ raise BaiduPanAPIError(
140
+ data.get("errno", errno),
141
+ data.get("errmsg", "Token refresh failed"),
142
+ request_id=data.get("request_id", ""),
143
+ )
144
+
145
+ raise BaiduPanAPIError(
146
+ errno,
147
+ data.get("errmsg", "Unknown error"),
148
+ request_id=data.get("request_id", ""),
149
+ )
150
+
151
+ return data
152
+
153
+ def close(self) -> None:
154
+ if self._session is not None:
155
+ self._session.close()
156
+ self._session = None