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:
175
sdk/mipu_requests/MIGRATION.md
Normal file
175
sdk/mipu_requests/MIGRATION.md
Normal 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"`
|
||||
|
||||
其余代码无需改动。
|
||||
41
sdk/mipu_requests/__init__.py
Normal file
41
sdk/mipu_requests/__init__.py
Normal 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",
|
||||
]
|
||||
33
sdk/mipu_requests/exceptions.py
Normal file
33
sdk/mipu_requests/exceptions.py
Normal 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}")
|
||||
72
sdk/mipu_requests/response.py
Normal file
72
sdk/mipu_requests/response.py
Normal 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)
|
||||
261
sdk/mipu_requests/session.py
Normal file
261
sdk/mipu_requests/session.py
Normal 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)
|
||||
Reference in New Issue
Block a user