refactor: 文档维护点收归mipu-api,新增SDK

- 移除 docs/ 和 Release CI,文档维护点收归 mipu-api 子模块
- 新增 sdk/mipu_requests Python SDK(构造参数传入凭据)
- 新增 CLAUDE.md 标注文档维护路径
- 精简 README

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
杨柳杰
2026-05-04 19:12:16 +08:00
parent 58b84d6f94
commit 67db2ee5b2
48 changed files with 804 additions and 5528 deletions

8
sdk/.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
__pycache__/
*.pyc
*.pyo
dist/
build/
*.egg-info/
.eggs/
config.json

21
sdk/LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 MipuYun
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.

126
sdk/README.md Normal file
View File

@@ -0,0 +1,126 @@
# mipu-requests
米普代理 HTTP 客户端,兼容 `requests` 接口。
所有 HTTP 请求通过米普 API 转发,接口与 `requests` 完全一致,迁移只需改 `import``proxies`
## 安装
```bash
pip install mipu-requests
```
## 快速开始
```python
import mipu_requests
s = mipu_requests.session(
base_url="https://api-eu.mipuyun.com",
client_key="your_client_key",
client_secret="your_client_secret",
agent="JQ",
)
s.proxies = "res_mipu_bookxWdyg"
resp = s.get("https://example.com/page")
resp.raise_for_status()
data = resp.json()
```
## 从 requests 迁移
```python
# ── 迁移前 ──────────────────────────
import requests
s = requests.Session()
s.proxies = {"https": "http://proxy:8080"}
resp = s.get("https://example.com/api")
# ── 迁移后 ──────────────────────────
import mipu_requests
s = mipu_requests.session(
base_url="https://api-eu.mipuyun.com",
client_key="your_key",
client_secret="your_secret",
agent="JQ",
)
s.proxies = "res_mipu_bookxWdyg"
resp = s.get("https://example.com/api")
```
只需两步:
1. `import requests``import mipu_requests`
2. 构造时传入凭据,`proxies` 改为字符串
其余代码(`.get()`, `.post()`, `.json()`, `.raise_for_status()` 等)无需改动。
## 请求方法
```python
resp = s.get(url, params={"key": "value"}, headers={...})
resp = s.post(url, data={"key": "value"}, headers={...})
resp = s.post(url, json={"key": "value"}, headers={...})
resp = s.put(url, data=body, headers={...})
resp = s.delete(url, headers={...})
```
## 异步模式
默认启用异步轮询,防止长连接断连:
```python
# 默认异步
s = mipu_requests.session(..., async_mode=True)
# 关闭异步
s = mipu_requests.session(..., async_mode=False)
# 自定义轮询参数
s = mipu_requests.session(
...,
poll_interval=3, # 轮询间隔(秒)
poll_max=20, # 最大轮询次数
submit_timeout=60, # 提交超时(秒)
poll_timeout=(5, 30), # 轮询超时 (connect, read)
)
```
## Response 对象
`requests.Response` 完全兼容:
```python
resp.status_code # int HTTP 状态码
resp.text # str 响应体文本
resp.content # bytes 响应体字节
resp.headers # dict 响应头
resp.ok # bool status_code 在 200-399
resp.url # str 请求 URL
resp.json() # dict 解析 JSON
resp.raise_for_status() # >=400 抛异常
```
## 异常处理
```python
from mipu_requests import RequestException, HTTPError, APIError, PollTimeoutError
try:
resp = s.get(url)
resp.raise_for_status()
except HTTPError as e:
print(f"目标站错误: {e.response.status_code}")
except APIError as e:
print(f"API 错误: {e}")
except PollTimeoutError as e:
print(f"轮询超时: {e}")
except RequestException as e:
print(f"请求异常: {e}")
```
## License
MIT

View File

@@ -0,0 +1,175 @@
# mipu_requests — 从 requests 迁移指南
## 快速对比
```python
# ── requests ──────────────────────────────────────
import requests
s = requests.Session()
s.proxies = {"https": "http://proxy:8080"}
resp = s.get("https://example.com/page")
resp.raise_for_status()
data = resp.json()
# ── mipu_requests ─────────────────────────────────
import mipu_requests
s = mipu_requests.session(
base_url="https://api-eu.mipuyun.com",
client_key="your_client_key",
client_secret="your_client_secret",
agent="JQ",
)
s.proxies = "res_mipu_bookxWdyg"
resp = s.get("https://example.com/page")
resp.raise_for_status()
data = resp.json()
```
## 安装
```bash
pip install mipu-requests
```
## 创建 Session
```python
import mipu_requests
# Agent 模式 — 通过指定 agent 走 /unlocker/agent 端点
s = mipu_requests.session(
base_url="https://api-eu.mipuyun.com",
client_key="your_key",
client_secret="your_secret",
agent="JQ",
)
# Request 模式 — 走 /unlocker/request 端点(无需 agent
s = mipu_requests.session(
base_url="https://api-eu.mipuyun.com",
client_key="your_key",
client_secret="your_secret",
mode="request",
)
```
## 设置代理
`requests` 唯一**必须修改**的地方:
```python
# requests — 传 dict
s.proxies = {"http": "http://proxy:8080", "https": "http://proxy:8080"}
# mipu_requests — 传字符串(米普 proxy 标识)
s.proxies = "res_mipu_bookxWdyg"
# 单次请求覆盖代理
resp = s.get(url, proxy="another_proxy")
```
## 请求方法
完全一致,无需改动:
```python
resp = s.get(url, params={"key": "value"}, headers={...})
resp = s.post(url, data={"key": "value"}, headers={...})
resp = s.post(url, json={"key": "value"}, headers={...})
resp = s.put(url, data=body, headers={...})
resp = s.delete(url, headers={...})
```
## Response 对象
属性和方法与 `requests.Response` 一致:
```python
resp.status_code # int HTTP 状态码
resp.text # str 响应体文本
resp.content # bytes 响应体字节
resp.headers # dict 响应头
resp.ok # bool status_code 在 200-399
resp.url # str 请求 URL
resp.encoding # str 编码(默认 utf-8
resp.cookies # dict 空 dict兼容属性
resp.reason # str 状态码原因短语
resp.json() # 解析 JSON
resp.raise_for_status() # >=400 抛异常
resp.iter_content(chunk_size) # 按块迭代
```
## 异常处理
```python
try:
resp = s.get(url)
resp.raise_for_status()
except mipu_requests.HTTPError as e:
print(f"目标站错误: {e.response.status_code}")
except mipu_requests.APIError as e:
print(f"API 错误: {e}")
except mipu_requests.RequestException as e:
print(f"请求异常: {e}")
```
## 异步模式
默认开启异步轮询防止长连接断连:
```python
# 默认异步
s = mipu_requests.session(...)
# 关闭异步
s = mipu_requests.session(..., async_mode=False)
# 自定义参数
s = mipu_requests.session(
...,
poll_interval=3,
poll_max=20,
submit_timeout=60,
poll_timeout=(5, 30),
)
```
## 完整迁移示例
```python
# ── 迁移前 (requests) ────────────────────────────
import requests
s = requests.Session()
s.headers.update({"user-agent": "Mozilla/5.0"})
s.proxies = {"https": "http://proxy:8080"}
resp = s.get("https://example.com/api", params={"page": 1})
resp.raise_for_status()
data = resp.json()
# ── 迁移后 (mipu_requests) ───────────────────────
import mipu_requests
s = mipu_requests.session(
base_url="https://api-eu.mipuyun.com",
client_key="your_key",
client_secret="your_secret",
agent="JQ",
)
s.headers = {"user-agent": "Mozilla/5.0"}
s.proxies = "res_mipu_bookxWdyg"
resp = s.get("https://example.com/api", params={"page": 1})
resp.raise_for_status()
data = resp.json()
```
**迁移只需 2 步:**
1. 构造 `mipu_requests.session()` 时传入 `base_url`/`client_key`/`client_secret`/`agent`
2. `s.proxies = {...}``s.proxies = "proxy_name"`
其余代码无需改动。

View File

@@ -0,0 +1,41 @@
"""
mipu_requests — 通过米普代理转发 HTTP 请求的 requests 兼容客户端
API 与 requests 完全一致,用户可平滑切换:
import mipu_requests
s = mipu_requests.session(
base_url="https://api-eu.mipuyun.com",
client_key="your_key",
client_secret="your_secret",
agent="JQ",
)
s.proxies = "res_mipu_bookxWdyg"
resp = s.get(url, headers={...}, params={...})
resp = s.post(url, data=form, headers={...})
resp.raise_for_status()
data = resp.json()
"""
from .session import session, Session
from .response import Response
from .exceptions import (
RequestException,
HTTPError,
APIError,
PollTimeoutError,
)
__version__ = "1.0.0"
__all__ = [
"session",
"Session",
"Response",
"RequestException",
"HTTPError",
"APIError",
"PollTimeoutError",
]

View File

@@ -0,0 +1,33 @@
"""mipu_requests 异常类 — 兼容 requests 异常命名"""
class RequestException(Exception):
"""基异常(对应 requests.exceptions.RequestException"""
pass
class APIError(RequestException):
"""Bluebird API 返回非零 code"""
def __init__(self, response):
self.response = response
code = response.get("code", "?") if isinstance(response, dict) else "?"
msg = response.get("msg", "") if isinstance(response, dict) else str(response)
super().__init__(f"API error: code={code}, msg={msg}")
class HTTPError(RequestException):
"""目标网站返回 HTTP 错误状态码(对应 requests.exceptions.HTTPError"""
def __init__(self, response):
self.response = response
super().__init__(f"{response.status_code} for {response.url}")
class PollTimeoutError(RequestException):
"""异步轮询超时"""
def __init__(self, agent_request_id, max_attempts=0):
self.agent_request_id = agent_request_id
self.max_attempts = max_attempts
super().__init__(f"Poll timeout after {max_attempts} attempts for {agent_request_id}")

View File

@@ -0,0 +1,72 @@
"""
Response — 兼容 requests.Response 的响应对象
属性/方法与 requests.Response 一致:
.status_code .text .content .headers .ok .url .encoding
.json() .raise_for_status() .iter_content()
"""
import json as _json
class Response:
"""兼容 requests.Response 的响应对象。
额外属性:
session_id: Bluebird 返回的 sessionId
raw: Bluebird 原始响应 dict
"""
def __init__(self, status_code=0, text="", headers=None, url="", raw=None):
self.status_code = int(status_code) if status_code else 0
self._text = text or ""
self.headers = dict(headers) if headers else {}
self.url = url
self.encoding = "utf-8"
self.raw = raw or {}
self.content = self._text.encode(self.encoding) if self._text else b""
self.ok = 200 <= self.status_code < 400
self.cookies = {}
@property
def text(self):
return self._text
def json(self, **kwargs):
return _json.loads(self._text, **kwargs)
def raise_for_status(self):
"""status_code >= 400 时抛出 HTTPError。"""
if not self.ok:
from .exceptions import HTTPError
raise HTTPError(self)
def iter_content(self, chunk_size=1):
"""兼容 requests.Response.iter_content()。"""
if self.content:
for i in range(0, len(self.content), chunk_size):
yield self.content[i:i + chunk_size]
@property
def session_id(self):
return self.raw.get("sessionId", "")
@property
def reason(self):
"""兼容 requests.Response.reason。"""
reasons = {
200: "OK", 201: "Created", 202: "Accepted",
301: "Moved Permanently", 302: "Found",
400: "Bad Request", 401: "Unauthorized", 403: "Forbidden",
404: "Not Found", 500: "Internal Server Error",
}
return reasons.get(self.status_code, "Unknown")
def __repr__(self):
return f"<Response [{self.status_code}]>"
def __bool__(self):
return self.ok
def __len__(self):
return len(self.content)

View File

@@ -0,0 +1,261 @@
"""
Session — 兼容 requests.Session 的米普代理客户端
用法与 requests.Session 一致:
import mipu_requests
s = mipu_requests.Session(
base_url="https://api-eu.mipuyun.com",
client_key="your_key",
client_secret="your_secret",
agent="JQ",
)
s.proxies = "res_mipu_bookxWdyg"
resp = s.get("https://example.com")
resp = s.post(url, data={"key": "val"}, headers={...})
resp.raise_for_status()
data = resp.json()
"""
import json as _json
import time
from urllib.parse import urlencode
import requests as _requests
from .exceptions import APIError, PollTimeoutError
from .response import Response
class Session:
"""兼容 requests.Session 的米普代理客户端。
Args:
base_url: 米普 API 地址 (如 "https://api-eu.mipuyun.com")
client_key: 客户端 key
client_secret: 客户端 secret
mode: "agent" → /unlocker/agent | "request" → /unlocker/request
agent: Agent 编码 (mode="agent" 时必传)
async_mode: 异步轮询 (默认 True)
poll_interval: 轮询间隔秒 (默认 3)
poll_max: 最大轮询次数 (默认 20)
submit_timeout:提交超时秒 (默认 60)
poll_timeout: 轮询超时 (connect, read) (默认 (5, 30))
"""
def __init__(
self,
base_url,
client_key,
client_secret,
mode="agent",
agent=None,
async_mode=True,
poll_interval=3,
poll_max=20,
submit_timeout=60,
poll_timeout=(5, 30),
):
self.base_url = base_url.rstrip("/")
self.client_key = client_key
self.client_secret = client_secret
if mode not in ("agent", "request"):
raise ValueError(f"mode 必须是 'agent''request',收到: {mode!r}")
if mode == "agent" and not agent:
raise ValueError("mode='agent' 时 agent 不能为空")
self.mode = mode
self._api_path = "/unlocker/agent" if mode == "agent" else "/unlocker/request"
self.agent = agent or None
self.async_mode = async_mode
self.poll_interval = poll_interval
self.poll_max = poll_max
self.submit_timeout = submit_timeout
self.poll_timeout = poll_timeout
self.proxies = None
self.headers = {}
self.session_id = None
self._http = _requests.Session()
# ── 核心请求方法 ────────────────────────────────────────────────
def request(self, method, url, *, params=None, data=None, json=None,
headers=None, content_type=None, proxy=None):
"""发送请求(兼容 requests.Session.request
Args:
method: HTTP 方法
url: 目标 URL
params: URL 查询参数 (dict)
data: 请求体 (dict → form-encoded, str → raw)
json: 请求体 (dict → JSON)
headers: 请求头 (dict)
content_type: Content-Type 覆盖
proxy: 本次请求代理覆盖
Returns:
Response
"""
proxy = proxy or self.proxies
if not proxy:
raise ValueError("未设置 proxy请通过 session.proxies = 'xxx' 或 request(proxy='xxx') 指定")
# 拼接 params
target_url = url
if params:
sep = "&" if "?" in target_url else "?"
target_url = target_url + sep + urlencode(params)
# 序列化 body
body = ""
ct = content_type
if json is not None:
body = _json.dumps(json, ensure_ascii=False)
ct = ct or "application/json"
elif data is not None:
if isinstance(data, dict):
body = urlencode(data)
ct = ct or "application/x-www-form-urlencoded"
else:
body = str(data)
# 合并 headers: session 默认 + 本次传入
merged_headers = {**self.headers, **(headers or {})}
# 构建 payload
payload = {
"url": target_url,
"method": method.upper(),
"proxy": proxy,
"async": self.async_mode,
}
if self.mode == "agent":
payload["agent"] = self.agent
if self.session_id:
payload["sessionId"] = self.session_id
if body:
payload["body"] = body
if ct:
payload["contentType"] = ct
if merged_headers:
payload["headers"] = merged_headers
# 调用米普 API
api_url = f"{self.base_url}{self._api_path}"
hdrs = {
"Content-Type": "application/json",
"client-key": self.client_key,
"client-secret": self.client_secret,
}
resp = self._http.post(api_url, json=payload, headers=hdrs,
timeout=self.submit_timeout)
api_resp = resp.json()
code = api_resp.get("code")
# 异步轮询
if code == 202 and self.async_mode:
agent_request_id = api_resp.get("agentRequestId", "")
time.sleep(2)
api_resp = self._poll_result(agent_request_id)
code = api_resp.get("code")
# 检查错误
if code != 0:
raise APIError(api_resp)
# 锁定 sessionId
sid = api_resp.get("sessionId", "")
if sid and not self.session_id:
self.session_id = sid
return Response(
status_code=api_resp.get("statusCode", 0),
text=api_resp.get("responseBody", ""),
headers=api_resp.get("responseHeaders", {}),
url=target_url,
raw=api_resp,
)
def get(self, url, **kwargs):
return self.request("GET", url, **kwargs)
def post(self, url, **kwargs):
return self.request("POST", url, **kwargs)
def put(self, url, **kwargs):
return self.request("PUT", url, **kwargs)
def delete(self, url, **kwargs):
return self.request("DELETE", url, **kwargs)
def head(self, url, **kwargs):
return self.request("HEAD", url, **kwargs)
def options(self, url, **kwargs):
return self.request("OPTIONS", url, **kwargs)
def patch(self, url, **kwargs):
return self.request("PATCH", url, **kwargs)
# ── 异步轮询 ────────────────────────────────────────────────────
def _poll_result(self, agent_request_id):
query_url = f"{self.base_url}/async/query/{agent_request_id}"
hdrs = {
"client-key": self.client_key,
"client-secret": self.client_secret,
"Connection": "close",
}
for attempt in range(1, self.poll_max + 1):
try:
s = _requests.Session()
s.headers.update(hdrs)
resp = s.get(query_url, timeout=self.poll_timeout)
s.close()
result = resp.json()
if result.get("code") == 0:
return result
except _requests.exceptions.RequestException:
pass
except ValueError:
pass
time.sleep(self.poll_interval)
raise PollTimeoutError(agent_request_id, self.poll_max)
# ── 生命周期 ────────────────────────────────────────────────────
def close(self):
self._http.close()
def __enter__(self):
return self
def __exit__(self, *args):
self.close()
def __repr__(self):
mode_info = f"agent={self.agent!r}" if self.mode == "agent" else "mode=request"
return (f"<Session({self.base_url!r}, {mode_info}, "
f"proxies={self.proxies!r})>")
def session(base_url, client_key, client_secret, **kwargs):
"""创建 Session 实例(与 requests.session() 用法一致)。
Args:
base_url: 米普 API 地址
client_key: 客户端 key
client_secret: 客户端 secret
**kwargs: 传递给 Session 的其他参数 (mode, agent, async_mode 等)
Returns:
Session
"""
return Session(base_url, client_key, client_secret, **kwargs)

38
sdk/pyproject.toml Normal file
View File

@@ -0,0 +1,38 @@
[build-system]
requires = ["setuptools>=64", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "mipu-requests"
version = "1.0.0"
description = "米普代理 HTTP 客户端,兼容 requests 接口"
readme = "README.md"
license = "MIT"
requires-python = ">=3.8"
authors = [
{ name = "MipuYun", email = "support@mipuyun.com" },
]
keywords = ["mipu", "requests", "proxy", "http", "api"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Internet :: WWW/HTTP",
]
dependencies = [
"requests>=2.20",
]
[project.urls]
Repository = "https://git.addhh.com/willow/mipu-open"
[tool.setuptools.packages.find]
include = ["mipu_requests*"]
[tool.setuptools.package-data]
mipu_requests = ["py.typed"]