Metadata-Version: 2.4
Name: never_primp
Version: 2.6.5
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.9
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: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Rust
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Software Development :: Libraries :: Python Modules
License-File: LICENSE
Summary: 基于原primp 用rust重构调整的python请求库 - The fastest python HTTP client that can impersonate web browsers
Keywords: requests,httpx,http,http-client,tls-fingerprint,ja3,ja4,impersonate,browser-impersonation,web-scraping,crawler,reverse-engineering
Author: Neverland
Requires-Python: >=3.9
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Bug Tracker, https://github.com/Neverland/never_primp/issues
Project-URL: Homepage, https://github.com/Neverland/never_primp
Project-URL: Repository, https://github.com/Neverland/never_primp

<div align="center">

# NEVER_PRIMP

**基于 Rust + wreq 的高性能 Python HTTP 客户端**
专为网络爬虫、浏览器指纹伪装与风控绕过设计

![Python >= 3.8](https://img.shields.io/badge/python->=3.8-blue.svg)
[![PyPI version](https://badge.fury.io/py/never-primp.svg)](https://pypi.org/project/never-primp)
[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
[![Rust](https://img.shields.io/badge/rust-1.80+-orange.svg)](https://www.rust-lang.org)

[安装](#安装) · [快速开始](#快速开始) · [性能原理](#性能优化原理深度解析) · [异步](#异步-asyncclient) · [流式响应](#流式响应) · [异常处理](#异常处理) · [Cookie 管理](#cookie-管理) · [浏览器伪装](#浏览器指纹伪装) · [API 参考](#api-参考)

</div>

---

## 为什么选择 NEVER_PRIMP？

| 功能 | NEVER_PRIMP | requests | httpx | curl-cffi |
|------|:-----------:|:--------:|:-----:|:---------:|
| 浏览器 TLS/JA3/JA4 指纹 | ✅ 100+ 配置 | ❌ | ❌ | ✅ 有限 |
| HTTP/2 指纹（AKAMAI）| ✅ | ❌ | ❌ | ✅ |
| 请求头顺序精确控制 | ✅ | ❌ | ❌ | ❌ |
| Cookie 分割（HTTP/2 风格）| ✅ | ❌ | ❌ | ❌ |
| 跨子域名 Cookie 共享 | ✅ RFC 6265 | ✅ | ✅ | ❌ |
| Cookie 跨会话持久化 | ✅ | ❌ | ❌ | ❌ |
| 高并发无锁 Client 共享 | ✅ | ❌ | ✅ | ❌ |
| GIL 释放（真实并行）| ✅ | ❌ | ✅ | ❌ |
| 真异步（asyncio，非线程池）| ✅ | ❌ | ✅ | ❌ |
| 流式响应（增量读 body）| ✅ | ✅ | ✅ | ✅ |
| 类型化异常（按错误类型重试）| ✅ | ✅ | ✅ | ❌ |
| 重试策略 | ✅ 内置预算 | ❌ | ❌ | ❌ |
| 文件上传（multipart）| ✅ | ✅ | ✅ | ✅ |

### 性能基准（测试 URL: https://www.baidu.com）

|  | requests | httpx | curl-cffi | never_primp |
|--|:--------:|:-----:|:---------:|:-----------:|
| 单次请求 | 646 ms | 90 ms | 122 ms | **86 ms** |
| 串行 10 次 | 655 ms | 20 ms | 47 ms | **19 ms** |
| 并发 100 任务 | 697 ms | 23 ms | 56 ms | **20 ms** |

---

## 安装

```bash
pip install -U never-primp
```

**平台支持**：Linux (x86_64/aarch64) · Windows (x86_64) · macOS (x86_64/ARM64)

**从源码构建**：
```bash
pip install maturin
maturin develop --release
```

---

## 快速开始

```python
import never_primp

# 最简单的用法
r = never_primp.get("https://httpbin.org/get")
print(r.status_code, r.json())

# 带浏览器指纹的 Client
client = never_primp.Client(
    impersonate="chrome_147",
    impersonate_os="windows",
    timeout=30.0,
)
r = client.get("https://httpbin.org/headers")
print(r.json())

# 上下文管理器（退出时自动关闭连接池）
with never_primp.Client(impersonate="firefox_149") as client:
    r = client.post("https://httpbin.org/post", json={"key": "value"})
    print(r.json())
```

---

## 性能优化原理深度解析

这一节详细解释 never_primp 每一项性能设计的底层原理。

### 1. 去除 `Arc<Mutex<Client>>`：消灭并发瓶颈

**旧版问题**

```rust
// 旧版：所有线程争同一把锁
pub struct RClient {
    client: Arc<Mutex<wreq::Client>>,  // 问题所在
}

// 每次发请求：
let resp = client.lock().unwrap().request(...).send().await;
//                ^^^^^^^^^^^
//                持锁期间，其他线程全部阻塞等待！
```

在 Python 的 ThreadPoolExecutor 场景下，20 个线程各自调用 `client.get()`：

```
线程1  ──[lock]──── request ──── [unlock]──
线程2             [等待] ────── request ──── [等待]...
线程3                       [等待] ...
                             串行化！
```

**新版方案**

wreq::Client 内部已用 `Arc` 包裹所有状态（连接池、Cookie Jar、配置），它实现了 `Clone + Send + Sync`：

```rust
// 新版：直接存储，clone 是零成本的 Arc 引用计数+1
pub struct RClient {
    client: wreq::Client,   // 内部已是 Arc<Inner>
}

// 请求时 clone 传入 async 块
let client = self.client.clone();  // 仅增加引用计数，O(1)，无锁
let future = async move {
    client.request(method, url).send().await
};
py.detach(|| runtime().block_on(future));  // GIL 释放后真正并行
```

20 个线程同时发请求：

```
线程1  ── clone(O1) ─── request ───────── 并行！
线程2  ── clone(O1) ─── request ───────── 并行！
线程3  ── clone(O1) ─── request ───────── 并行！
           ↑ 每个 clone 只是原子计数+1，互不干扰
```

**性能影响**：高并发场景下吞吐量从串行变为真正并行，延迟从 O(N×T) 降为 O(T)。

---

### 2. GIL 释放：Python 多线程的正确姿势

Python 的 GIL（Global Interpreter Lock）保证同一时刻只有一个线程执行 Python 字节码，但 IO 操作期间可以释放。

**never_primp 的实现**：

```rust
// 进入 IO 之前显式释放 GIL
let wreq_response = py.detach(|| {
    runtime().block_on(future)  // 整个 HTTP 请求期间 GIL 释放
});
// GIL 在这里自动重新获取
```

`py.detach()` 等价于在 C 扩展中调用 `Py_BEGIN_ALLOW_THREADS`，让其他 Python 线程在等待 HTTP 响应期间正常运行。

**实际效果**：

```
GIL 持有时间线（旧版无释放）：
Thread1: [GIL][send HTTP][wait response][GIL] ...
Thread2:            [等待GIL..............][GIL][send HTTP]...
结果：几乎串行

GIL 释放时间线（新版）：
Thread1: [GIL][send HTTP─────────]→[wait resp in Tokio]→[GIL][return]
Thread2:      [GIL][send HTTP─────]→[wait resp in Tokio]→[GIL][return]
Thread3:           [GIL][send HTTP]→[wait resp in Tokio]→[GIL][return]
结果：真正并行
```

GIL 只在构建请求和处理响应这两个极短的 CPU 阶段被持有，网络 IO 等待期间全部并行。

---

### 3. Tokio 异步运行时：4 Worker 线程的选择

```rust
// src/runtime.rs —— 同步与异步两条路径共用同一个运行时
pub fn init_runtime() {
    let mut builder = tokio::runtime::Builder::new_multi_thread();
    builder.enable_all().worker_threads(4).thread_name("never-primp-worker");
    pyo3_async_runtimes::tokio::init(builder);   // 在 #[pymodule] 初始化时调用一次
}
pub fn runtime() -> &'static tokio::runtime::Runtime {
    pyo3_async_runtimes::tokio::get_runtime()
}
```

> **同步路径** 用 `runtime().block_on(...)`；**异步路径**（`AsyncClient`）用 `pyo3_async_runtimes::tokio::future_into_py(...)` 把 Rust future 桥接到 asyncio。两者共用同一个 4-worker 运行时，几千个并发请求多路复用其上，**不再每请求占用一个 OS 线程**（详见 [异步](#异步-asyncclient)）。

**为什么是 4 而不是更多？**

Tokio 的 worker 线程处理的是 IO 事件（socket 可读/可写的通知），而不是实际等待数据。HTTP 请求的主要时间消耗在网络 RTT，不是 CPU 计算：

```
典型 HTTP 请求生命周期：
  建立连接: ~5ms  (CPU: <1ms)
  发送请求: ~0.1ms (CPU: <0.1ms)
  等待响应: ~50ms  (CPU: 0，纯等待)
  读取响应: ~1ms   (CPU: <0.5ms)

4 个 worker 可以同时管理数千个 "等待响应" 状态的 socket，
因为 epoll/IOCP 一次系统调用可以轮询所有就绪事件。
```

对于 CPU 密集的任务应该用 `spawn_blocking`，对于 IO 密集的请求任务，4 个 worker 通常已经饱和。增加到 16 个 worker 对 IO bound 场景几乎无提升，反而增加线程切换开销。

---

### 4. 连接池：TCP 复用的关键参数

```rust
client_builder
    .pool_max_idle_per_host(32)        // 默认：每主机保留 32 条空闲 TCP 连接（可配置）
    .pool_max_size(512)                // 默认：全局上限 512 条（可配置）
    .pool_idle_timeout(Duration::from_secs(30))  // 默认：30s 未使用则关闭（可配置）
    .tcp_keepalive(Duration::from_secs(30))       // SO_KEEPALIVE
    .tcp_keepalive_interval(Duration::from_secs(15))
    .tcp_keepalive_retries(3u32)
    .tcp_nodelay(true);                // 禁用 Nagle 算法
```

三个连接池参数现在可通过 Python 层按场景调整：

```python
# 多域名爬取（默认值已适合此场景）
client = Client(impersonate="chrome_147", cookie_store=False)

# 单域名高并发（提高单 host 连接数）
client = Client(
    impersonate="chrome_147",
    pool_max_idle_per_host=64,   # 每 host 最多 64 个空闲连接
    pool_max_size=256,
    pool_idle_timeout=60.0,
)
```

**为什么这些参数很重要？**

**TCP 连接复用（最大收益）**：

建立一条 TCP+TLS 连接的开销：
```
TCP 三次握手:  ~10ms (1 RTT)
TLS 1.3 握手: ~15ms (1 RTT)
合计:          ~25ms
```

如果每个请求都新建连接（pool 为 0），100 个请求就浪费 2500ms 在握手上。连接池让同一主机的请求复用已建立的连接，第 2 次请求开始几乎没有建连开销。

**TCP Keepalive（连接稳定性）**：

```
没有 Keepalive 的问题：
  服务器/NAT/防火墙在 60-120s 内无流量会 silently drop 连接
  下次使用这条"死连接"时：RST 或超时，请求失败

SO_KEEPALIVE 工作原理：
  每 30s 发一个 TCP ACK 探测包（几乎无流量）
  服务器响应 → 连接确认活跃，重置超时计时器
  服务器无响应 → 15s 后重试，最多 3 次，才判定连接断开

效果：空闲连接保持真正可用，避免"僵尸连接"导致的请求失败
```

**TCP_NODELAY（降低小包延迟）**：

Nagle 算法会把小数据包积攒到 MSS（约 1460 字节）再发送，降低延迟对 HTTP 不友好。`tcp_nodelay(true)` 禁用它，每次 `write()` 立即发送，降低请求延迟约 10-40ms。

---

### 5. Cookie Jar 的 RFC 6265 实现

wreq 的 `Jar` 内部结构：

```
HashMap<domain, HashMap<path, CookieJar>>
   ↑ 按 (domain, path) 二级索引存储
```

**子域名 Cookie 共享（domain_match）**：

```rust
fn domain_match(host: &str, domain: &str) -> bool {
    host == domain                          // 完全匹配
    || (host.len() > domain.len()
        && host.ends_with(domain)
        && host.as_bytes()[host.len() - domain.len() - 1] == b'.')
    //  ↑ 确保是真子域名：api.example.com 匹配 example.com
    //    但 notexample.com 不匹配 example.com
}
```

查询 `api.example.com` 的 cookie 时，遍历所有 domain key 做 domain_match：
- `example.com` → 匹配，返回其 cookie
- `api.example.com` → 完全匹配，也返回

**读写锁而非互斥锁**：

```rust
store: Arc<RwLock<HashMap<...>>>
//         ↑ 读写锁

// 查询时（大多数操作）：多个线程并发读，互不阻塞
store.read().get(host)...

// 修改时（Set-Cookie 响应）：独占写锁
store.write().entry(domain).or_default()...
```

爬虫场景下读（发请求带 Cookie）远多于写（收到 Set-Cookie），RwLock 比 Mutex 在并发读时效率高得多。

**大并发内存注意事项**：

`cookie_store=True`（默认）时，所有 `Set-Cookie` 响应头都会写入 Jar，且**永不自动过期**。在高并发打多个不同域名的场景下：
- Jar 随域名数量持续增长（无上限）
- 每次写入（收到 Set-Cookie）都要竞争独占写锁，成为并发瓶颈

**结论：无状态爬取场景使用 `cookie_store=False`，可显著降低内存占用和锁争用。**

---

### 6. HTTP/2 连接序言合并（反检测关键）

现代风控系统会检测 HTTP/2 握手时第一次 TCP 应用数据的大小。真实浏览器（如 Safari）会将连接序言、SETTINGS、WINDOW_UPDATE、首个 HEADERS 帧合并为**一次 TCP 突发**：

```
真实 Safari 握手：
  第 1 次 TCP 应用数据 [424 bytes]:
    PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n  (24 bytes, 连接序言)
    SETTINGS frame                     (51 bytes)
    WINDOW_UPDATE frame                (13 bytes)
    HEADERS frame                     (~336 bytes, 首个请求)
  → 风控检测通过 ✅

旧版实现握手（显式 flush 导致拆包）：
  第 1 次 TCP 应用数据 [70 bytes]:
    PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n  (24 bytes)
    SETTINGS frame                     (51 bytes)
    WINDOW_UPDATE frame                (13 bytes)
  第 2 次 TCP 应用数据 [~298 bytes]:
    HEADERS frame
  → 风控检测失败 ❌  "[!] Safari detected but invalid SM packet (Len=70). Marking as bot."
```

never_primp 的底层 HTTP/2 实现移除了连接建立时的提前 flush，确保连接序言和首个请求帧在同一 TCP 段中发出，与浏览器行为完全一致。

---

### 7. 重试预算机制

```rust
// 内置令牌桶重试预算
let policy = RetryPolicy::default()
    .max_retries_per_request(2);  // 每请求最多重试 2 次
```

wreq 的重试策略内置了**预算限制**（默认 20% 额外负载）：

```
假设发出 1000 个请求：
  正常请求: 1000
  允许的重试: 1000 × 20% = 200

如果某段时间重试过多（>200），
预算耗尽，后续请求即使失败也不再重试，
避免雪崩效应（retry storm）导致服务器更不稳定。
```

这比简单的 `for _ in range(3): try: request()` 更智能，在大并发场景下保护目标服务器。

---

### 8. 头部顺序控制（anti-bot 核心）

现代风控系统（Cloudflare、Akamai、PerimeterX）会检测请求头的顺序作为 bot 指纹：

```
真实 Chrome 143 的头部顺序：
  :method: GET
  :authority: example.com
  :scheme: https
  :path: /
  sec-ch-ua: ...
  sec-ch-ua-mobile: ?0
  sec-ch-ua-platform: "Windows"
  upgrade-insecure-requests: 1
  user-agent: Mozilla/5.0...
  accept: text/html,...
  sec-fetch-site: none
  sec-fetch-mode: navigate
  sec-fetch-user: ?1
  sec-fetch-dest: document
  accept-encoding: gzip, deflate, br, zstd
  accept-language: zh-CN,...
```

never_primp 用 `OrigHeaderMap` 记录头部插入顺序：

```rust
// 普通 HeaderMap 只保证 O(1) 查找，不保证顺序
// OrigHeaderMap 是有序列表，精确控制发送顺序
let mut orig_headers = OrigHeaderMap::new();
for (key, _) in client_headers.iter() {
    orig_headers.insert(key.clone());  // 保留插入顺序
}
request_builder = request_builder.orig_headers(orig_headers);
```

这确保了发出去的 TCP 字节流中头部顺序与真实浏览器完全一致。

---

## Cookie 管理

`client.cookies` 返回一个 `RequestsCookieJar` 对象，提供类 dict 接口，行为与 `requests.Session.cookies` 一致。内部分为两层：
- **全局 Cookie**：无域名限定，随所有请求发送
- **域名 Cookie**：由 `Set-Cookie` 响应头自动写入，或手动通过 `set(domain=…)` 添加，按 RFC 6265 规则匹配

### 自动跨子域名共享

```python
client = never_primp.Client(impersonate="chrome_147")

# 登录后，服务器设置 domain=.example.com 的 Cookie
client.get("https://example.com/login")  # 自动存储 Set-Cookie

# 访问子域名时，这些 Cookie 自动发送（RFC 6265）
client.get("https://api.example.com/data")   # 自动带 Cookie
client.get("https://cdn.example.com/asset")  # 自动带 Cookie
```

### 全局 Cookie 操作（类 dict）

```python
jar = client.cookies

# 读写删（全局 Cookie，随所有请求发送）
jar["token"] = "abc"
val = jar["token"]
del jar["token"]
"token" in jar           # True / False

# 批量读写
jar.update({"a": "1", "b": "2"})
print(jar.get_dict())    # {"a": "1", "b": "2"}

# 遍历
for name, value in jar.items():
    print(name, value)

jar.clear_global()       # 只清空全局 Cookie
jar.clear_jar()          # 只清空域名 Cookie
jar.clear()              # 清空所有
```

### 域名 Cookie 操作

```python
# 设置域名 Cookie（只发送给匹配的域名，RFC 6265）
client.cookies.set("auth_token", "eyJhbGci...",
                   domain="example.com", path="/")

# 查询会发送到某 URL 的全部 Cookie
cookies = client.cookies.get_cookies_for_url("https://api.example.com/data")

# 按名称跨域名/路径查找（适用于不知道确切 path 的情况）
val = client.cookies.find("sec_cpt")

# 查看所有域名 Cookie 的完整元数据
for name, value, domain, path in client.cookies.get_jar_cookies():
    print(f"[{domain}{path}] {name}={value}")
```

### 跨会话 Cookie 持久化

```python
import json

# 会话一：登录
client = never_primp.Client(impersonate="chrome_147")
client.get("https://example.com/login")

# 导出并保存（包含全局 Cookie 和域名 Cookie）
with open("session.json", "w") as f:
    json.dump(client.export_cookies(), f)

# ─── 程序重启 ───

# 会话二：恢复登录状态
client2 = never_primp.Client(impersonate="chrome_143")
with open("session.json") as f:
    client2.import_cookies([tuple(c) for c in json.load(f)])

# 直接访问需要登录的页面
r = client2.get("https://example.com/dashboard")
```

---

## 浏览器指纹伪装

### 支持的浏览器（100+ 配置）

| 浏览器 | 版本范围 | 别名 |
|--------|---------|------|
| Chrome | 100–148 | `"chrome"` → 最新 |
| Edge | 101–148 | `"edge"` → 最新 |
| Firefox | 109–151 | `"firefox"` → 最新 |
| Safari macOS | 15.3–26.4 | `"safari"` → 最新 |
| Safari iOS | 16.5–26.2 | `"safari_ios"` → 最新 |
| Safari iPad | 18–26.2 | `"safari_ipad"` → 最新 |

> **Safari 26+ TLS 说明**：Safari 26.x 使用 BoringSSL 后端，相比旧版有以下变化：
> - Cipher suites 顺序调整为 AES-256-GCM → ChaCha20 → AES-128-GCM
> - 支持 **X25519MLKEM768** 后量子混合密钥交换（`supported_groups` + `key_share`）
> - `supported_versions` 仅包含 TLS 1.2 和 TLS 1.3（不再宣告 TLS 1.0/1.1）
> - `accept-encoding` 值更新为 `gzip, deflate, br, zstd`（新增 zstd）
| Opera | 116–131 | `"opera"` → 最新 |
| OkHttp | 3.9–5 | `"okhttp"` → 最新 |

### 伪装内容

```python
client = never_primp.Client(
    impersonate="chrome_143",
    impersonate_os="windows",   # windows / macos / linux / android / ios
)
```

每个配置包含：
- **TLS 指纹**：cipher suites 顺序、TLS extensions、椭圆曲线、签名算法（JA3/JA4）
- **HTTP/2 指纹**：SETTINGS 帧参数、WINDOW_UPDATE、HEADERS 帧顺序（AKAMAI 指纹）
- **请求头集合**：与该浏览器版本完全一致的默认头部（含正确的 `accept-encoding` 值与位置）
- **请求头顺序**：精确匹配浏览器的头部发送顺序
- **H2 连接预热合并**：连接序言 + SETTINGS + WINDOW_UPDATE 与首个 HEADERS 帧合并为单次 TCP 突发，与真实浏览器行为一致

### Cookie 分割（HTTP/2 浏览器行为）

```python
# HTTP/2 中浏览器将每个 Cookie 作为独立的 Header Frame 字段发送
client = never_primp.Client(
    impersonate="chrome_143",
    split_cookies=True,     # 默认 True（Python 层）
)

# 发出的 HTTP/2 HEADERS 帧：
# cookie: session=abc
# cookie: user_id=123
# cookie: csrf_token=xyz
```

---

## 异步 (AsyncClient)

`AsyncClient` 是**真正的异步**：每个请求返回一个由 Rust future 桥接（经 `pyo3-async-runtimes`）到当前 asyncio 事件循环的 awaitable，由共享的 4-worker Tokio 运行时驱动。成千上万的并发请求在少量 worker 上多路复用，**不再每个请求占用一个 OS 线程**——`asyncio.gather` 的并发量远超旧版线程池包装（旧版 `run_in_executor` 默认上限约 32）。

接口与同步 `Client` 完全一致，只是方法需 `await`：

```python
import asyncio
import never_primp

async def main():
    async with never_primp.AsyncClient(impersonate="chrome_147") as client:
        # 单个请求
        r = await client.get("https://httpbin.org/get")
        print(r.status_code, r.json())

        # 大并发：一次性发起成百上千个请求
        urls = [f"https://httpbin.org/anything/{i}" for i in range(500)]
        results = await asyncio.gather(
            *(client.get(u) for u in urls),
            return_exceptions=True,   # 单个失败不影响整体
        )
        ok = sum(1 for x in results if not isinstance(x, Exception))
        print(f"{ok}/{len(urls)} 成功")

asyncio.run(main())
```

> 并发轮换指纹/代理同样适用：`await client.get(url, impersonate="firefox_135", proxy="http://...")`。相同指纹的请求会复用缓存的连接池（见 [请求方法](#请求方法)）。

---

## 流式响应

默认情况下响应体会被一次性读入内存。对于**大文件下载**或**分块/SSE 流式响应**，传入 `stream=True` 即可增量读取——此时返回的是 `StreamResponse`，连接保持打开直到读完或 `close()`。

**同步（`Client`）**：

```python
with never_primp.Client(impersonate="chrome_147") as client:
    r = client.get("https://example.com/large-file.bin", stream=True)
    print(r.status_code, r.headers)        # 头部立即可用，body 尚未读取

    with open("large-file.bin", "wb") as f:
        # chunk_size=None：产出网络原生分片；指定整数：产出定长分片（末块可能更小）
        for chunk in r.iter_bytes(chunk_size=65536):
            f.write(chunk)

    # 或一次性读取剩余全部
    # data = r.read()
```

**异步（`AsyncClient`）**：

```python
async with never_primp.AsyncClient(impersonate="chrome_147") as client:
    r = await client.get("https://example.com/stream", stream=True)
    async for chunk in r.aiter_bytes():    # 逐块异步消费
        process(chunk)
    # 或：body = await r.aread()
```

`StreamResponse` 接口：

| 成员 | 说明 |
|------|------|
| `status_code` / `url` / `headers` / `cookies` / `encoding` | 与 `Response` 一致，发送后立即可用 |
| `iter_bytes(chunk_size=None)` | 同步迭代器；`for chunk in r.iter_bytes(): ...` |
| `aiter_bytes(chunk_size=None)` | 异步迭代器；`async for chunk in r.aiter_bytes(): ...` |
| `read()` / `await aread()` | 读取并返回剩余全部 body 字节 |
| `close()` | 提前关闭流，释放底层连接（不再使用时也可直接丢弃对象） |

> 高并发下请尽快消费流（连接在读完前一直占用），或对不需要的响应调用 `close()`。

---

## 异常处理

请求**执行期间**的失败会抛出类型化异常，便于按错误类型决定重试 / 切换代理，无需匹配错误字符串：

```
HTTPError                 # 基类，可一把捕获所有请求错误
├── ConnectionError       # DNS / 连接 / TLS 握手 / 代理 / 连接重置 / 请求未发出
├── TimeoutError          # 请求或读取超时
├── StatusError           # error_for_status 触发的 4xx / 5xx
└── DecodeError           # body 读取或解码失败
```

```python
import never_primp
from never_primp import HTTPError, ConnectionError, TimeoutError

client = never_primp.Client(impersonate="chrome_147", timeout=10.0)

def fetch_with_retry(url, proxies, retries=3):
    for attempt in range(retries):
        try:
            return client.get(url, proxy=proxies.current)
        except TimeoutError:
            continue                       # 超时：直接重试
        except ConnectionError:
            proxies.rotate()               # 连接/代理故障：换代理再试
        except HTTPError:
            raise                          # 其它请求错误：交给上层
    raise RuntimeError("重试次数耗尽")
```

> 这些异常类挂在包命名空间下（`never_primp.TimeoutError` 等），是 Python 内建同名异常的**独立类型**。配置类错误（如无效的浏览器名、非法 URL）仍抛 `RuntimeError` / `ValueError`。

---

## API 参考

### Client 构造参数

```python
client = never_primp.Client(
    # 认证
    auth=("username", "password"),  # Basic Auth
    auth_bearer="token",            # Bearer Auth

    # 网络
    proxy="socks5://127.0.0.1:1080",
    timeout=30.0,                   # 总超时（秒）
    verify=True,                    # SSL 证书验证
    ca_cert_file="/path/to/ca.pem", # 自定义 CA 证书

    # 浏览器伪装
    impersonate="chrome_147",
    impersonate_os="windows",       # windows/macos/linux/android/ios

    # HTTP 协议
    http1_only=False,               # 强制 HTTP/1.1
    http2_only=False,               # 强制 HTTP/2
    https_only=False,               # 拒绝 HTTP 请求
    follow_redirects=True,
    max_redirects=20,

    # Cookie
    # cookie_store=True 时，所有 Set-Cookie 响应头被持久化并自动发送
    # 大并发无状态抓取建议设为 False，显著降低内存和锁争用
    cookie_store=True,
    split_cookies=True,             # HTTP/2 风格 Cookie 头

    # 重试
    max_retries=2,                  # 网络错误时的最大重试次数

    # 连接池（可选，有合理默认值）
    pool_max_idle_per_host=32,      # 每 host 最多保留的空闲连接数（默认 32）
    pool_max_size=512,              # 全局连接总上限（默认 512）
    pool_idle_timeout=30.0,         # 空闲连接关闭前的等待秒数（默认 30）

    # 默认值（所有请求共享）
    headers={"X-Custom": "value"},
    params={"version": "2"},
)
```

### 请求方法

```python
# 所有方法支持相同的参数集
r = client.get(url,
    params={"q": "python"},
    headers={"Accept": "application/json"},
    cookies={"session": "abc"},
    timeout=10.0,
    proxy="http://127.0.0.1:8080",
    verify=False,
    # 请求级别临时覆盖
    impersonate="firefox_147",
    impersonate_os="linux",
)

r = client.post(url,
    json={"key": "value"},            # JSON 请求体
    # data={"key": "value"},          # Form 表单
    # content=b"raw bytes",           # 原始字节
    # files={"file": "/path/to/file"},# 文件上传（multipart）
)

# stream=True 返回 StreamResponse，增量读取 body（见“流式响应”）
r = client.get(url, stream=True)
```

> **请求级指纹覆盖会被缓存复用**：当某次请求带了 `impersonate` / `verify` / `http1_only` 等覆盖参数时，会按这组参数构建并**缓存**一个独立的客户端（含连接池）。后续相同指纹的请求复用同一缓存客户端，从而保留 TCP/TLS 连接复用——轮换指纹不再每次重建客户端。`proxy` / `timeout` 不参与缓存键（在请求级单独应用），因此代理轮换始终是廉价的。`close()` 会清空该缓存。

### Response 对象

```python
r = client.get("https://httpbin.org/get")

r.status_code      # int: 200
r.url              # str: 最终 URL（重定向后）
r.headers          # dict[str, str]: 响应头
r.cookies          # dict[str, str]: Set-Cookie 解析结果
r.content          # bytes: 原始响应体
r.text             # str: 自动编码检测后的文本
r.encoding         # str: 检测到的编码
r.json()           # Any: JSON 解析
```

### Cookie 管理（通过 `client.cookies`）

`client.cookies` 返回 `RequestsCookieJar`，提供以下接口：

```python
jar = client.cookies  # RequestsCookieJar

# ── 类 dict 接口（全局 Cookie）──────────────────────────────────
jar["name"]                        # → str，获取
jar["name"] = "value"              # 设置
del jar["name"]                    # 删除
"name" in jar                      # → bool
len(jar)                           # → int
iter(jar)                          # 遍历所有 name

jar.get(name, default=None)        # → str | None
jar.update({"k": "v"})            # 批量设置全局 Cookie
jar.keys()                         # → list[str]
jar.values()                       # → list[str]
jar.items()                        # → list[tuple[str, str]]
jar.get_dict()                     # → dict[str, str]，全部 Cookie 序列化

# ── 域名 Cookie ─────────────────────────────────────────────────
jar.set(name, value,               # 设置域名 Cookie
        domain=None, path=None)
jar.get_jar_cookies()              # → list[tuple[name, value, domain, path]]
jar.get_cookies_for_url(url)       # → dict[str, str]，RFC 6265 匹配
jar.find(name, default=None)       # → str | None，跨域名/路径查找

# ── 清空 ─────────────────────────────────────────────────────────
jar.clear_global()                 # 清空全局 Cookie
jar.clear_jar()                    # 清空域名 Cookie
jar.clear()                        # 清空全部

# ── 跨会话持久化（在 Client 上）────────────────────────────────
client.export_cookies()            # → list[tuple[name, value, domain, path]]
client.import_cookies([(name, value, domain, path)])  # → None
```

### 请求头管理方法

```python
client.headers = {"User-Agent": "bot"}       # 设置（替换全部）
client.headers                               # 读取
client.set_header("X-Custom", "value")       # 设置单个
client.get_header("X-Custom")               # 读取单个 → str | None
client.headers_update({"Accept": "*/*"})     # 合并更新
client.delete_header("X-Custom")             # 删除单个
client.clear_headers()                       # 清空全部
```

### 会话关闭

```python
# 方式一：上下文管理器（推荐）
# __exit__ / __aexit__ 会自动调用 close()，立即释放空闲 TCP 连接
with Client(impersonate="chrome_147", cookie_store=False) as client:
    r = client.get("https://example.com")

# 方式二：手动关闭
client = Client(impersonate="chrome_147")
# ... 若干请求 ...
client.close()   # 释放连接池；正在进行中的请求不受影响

# 注意：CPython 的引用计数机制保证 Client 对象离开作用域时
# 即时触发 Drop，连接池会自然释放。close() 的价值在于：
# 1. 明确语义（对熟悉 requests.Session.close() 的用户）
# 2. 处理全局变量或引用循环持有 Client 的边界情况
# 3. 使 with 语句具有实际清理效果（而不是空操作）
```

### 便利函数（无需创建 Client）

```python
import never_primp

r = never_primp.get("https://httpbin.org/get")
r = never_primp.post("https://httpbin.org/post", json={"a": 1})
r = never_primp.put(url, data={"k": "v"})
r = never_primp.delete(url)
r = never_primp.patch(url, json={})
r = never_primp.head(url)
r = never_primp.options(url)
```

### AsyncClient（异步接口）

构造参数与 `Client` 完全相同；所有请求方法为 `async`，需 `await`。这是真异步（非线程池），适合 `asyncio.gather` 大并发，并支持 `stream=True`。详见 [异步](#异步-asyncclient) 与 [流式响应](#流式响应)。

```python
import asyncio
import never_primp

async def main():
    async with never_primp.AsyncClient(
        impersonate="chrome_147",
        max_retries=2,
    ) as client:
        r = await client.get("https://httpbin.org/get")
        print(r.json())

        # 大并发
        rs = await asyncio.gather(*(client.get(u) for u in urls))

        # 流式
        s = await client.get(big_url, stream=True)
        async for chunk in s.aiter_bytes():
            ...

asyncio.run(main())
```

---

## 开发

```bash
# 环境依赖
pip install maturin

# 开发构建（快速迭代）
maturin develop

# 发布构建（完整优化）
maturin develop --release

# 代码检查
cargo check
cargo clippy
cargo fmt

# 运行示例
python example/concurrent_requests.py
python example/cookie_management.py
python example/browser_impersonation.py
```

---

## 架构概览

```
Python 调用  client.get(url)              AsyncClient:  await client.get(url)
      ↓                                          ↓
Client.__init__.py   # Python 封装层（ergonomics）
      ↓                                          ↓
RClient.request()                        RClient.request_async()   # src/client.rs
      ↓                                          ↓
build_request_future()  # 共享：GIL 内完成所有准备，产出 Send future
      ↓                                          ↓
py.detach()+runtime().block_on()         future_into_py()  # 桥接到 asyncio
      ↓ （释放 GIL）                              ↓ （事件循环驱动）
            └──────────────┬───────────────────┘
                           ↓
              共享 Tokio 运行时（4 workers）
                           ↓
wreq::Client        # Rust HTTP 客户端（内部 Arc，无锁 clone）
                           ↓
wreq 连接池          # TCP 复用 + TLS 会话复用（含请求级指纹覆盖的缓存客户端）
                           ↓
目标服务器
                           ↓
finish_request()  # 缓冲→Response 或 stream=True→StreamResponse（src/response.rs）
                           ↓
返回 Python，GIL 重新获取
```

---

## License

MIT

