Metadata-Version: 2.4
Name: never_safari
Version: 0.5.3
Classifier: Programming Language :: Rust
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Intended Audience :: Developers
Summary: Send local HTTP requests with a capture-validated macOS Safari 26.5.2 network fingerprint.
Keywords: safari,tls,http2,fingerprint,ja4,anti-detection,scraping
Author: never_safari contributors
License: MIT
Requires-Python: >=3.9
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Repository, https://github.com/neverl805/never_safari

# never_safari (Python)

Send local HTTP requests with a **capture-validated macOS Safari 26.5.2 static
network fingerprint** — TLS ClientHello (JA3/JA4), HTTP/2 (SETTINGS/WINDOW_UPDATE/
pseudo-header order), HPACK (Huffman + dynamic table), request-header order,
download flow-control cadence, and TLS-record framing — like `curl_cffi`, but for
Safari. Built on a patched BoringSSL plus from-scratch HTTP/1.1 and HTTP/2
layers, with opportunistic HTTP/3 and automatic protocol fallback.

## Install

```sh
pip install never_safari          # prebuilt wheels: macOS / Linux / Windows
# or from source (needs cmake, go, ninja, a C/C++ compiler):
pip install maturin && maturin develop -m crates/safari-py/Cargo.toml
```

## Use — a `requests`-style API

```python
import never_safari

# Session keeps cookies + a connection pool (like a browser). `Client` is an alias.
s = never_safari.Session()
r = s.get("https://tls.peet.ws/api/all", params={"pretty": "1"})
print(r.status_code, r.ok)
print(r.json()["tls"]["ja4"])       # t13d2013h2_a09f3c656075_7f0f34a4126d

# POST: json= takes a Python object; data= takes a dict (form) / str / bytes.
r = s.post("https://api.example.com/v1/items",
           json={"name": "x"},
           headers={"authorization": "Bearer TOKEN"})
r.raise_for_status()                # raises never_safari.HTTPError on 4xx/5xx

# Module-level, exactly like requests:
never_safari.get("https://example.com", params={"q": "1"})
never_safari.post("https://api.example.com", data={"k": "v"}, timeout=10)

# Session options
s = never_safari.Session(
    verify=True,          # TLS cert verification (like requests)
    proxies={"https": "socks5://user:pass@host:1080"}, # or HTTP CONNECT
    http3=True,          # default: H3 preference, then ALPN-selected H2/H1.1
    multiplex=True,       # default: concurrent requests share one connection
    accept_language="en-US,en;q=0.9", # match the proxy/browser locale
    timeout=30.0, connect_timeout=30.0, idle_timeout=60.0, max_redirects=10,
)

print(r.http_version)   # "h3", "h2", or "http/1.1"
print(s.resource_stats) # live request/dial/cache counters
# `with never_safari.Session(...) as s:` or `s.close()` releases them promptly.
# Crawler resource limits are enabled automatically; constructor overrides are
# available only when a workload needs non-default tuning.
# After an OS/network change, validate and switch a live H3 path:
new_local_address = s.migrate_http3("https://example.com")
```

Matches requests' surface:

- **Verbs**: `get/post/put/patch/delete/head/options/request` — on the `Session`
  and at module level.
- **Request kwargs**: `params=`, `data=` (dict → form / str / bytes), `json=`
  (any object), `headers=`, `cookies=`, `referer=`, `mode=` (`auto`,
  `navigation`, or `fetch`), `destination=` (`auto`, `document`, `empty`, or
  capture-validated `image`), `top_level_url=`, `referrer_policy=`, `timeout=`
  (seconds, per-request), `allow_redirects=` (default `True`). Use
  `mode="navigation"` for a top-level browser load and `mode="fetch"` for XHR.
  For a browser image request, pass `destination="image"` together with its
  `top_level_url` and `referer`.
  `params` accepts either a mapping or `(name, value)` pairs and expands
  sequence values. `data` and `json` are mutually exclusive; unsupported body
  types raise instead of silently sending an empty body.
- **Headers**: supplied values are authoritative. Known Safari fields are
  checked and automatically reordered into the captured profile; only omitted
  fields receive defaults. Custom fields retain caller order/anchors. An
  explicit `headers={"cookie": ...}` replaces the jar for that request, while
  `cookies=` augments it. Jar cookies are joined into one field on H1 and split
  into one incrementally indexed field per pair on H2, matching live Safari;
  an explicit Cookie remains intact. Names are lowercased for H2/H3 and use Safari's
  captured canonical casing for HTTP/1.1. Connection-specific/Host fields are
  removed for protocol correctness; an explicit Content-Length must equal the
  actual body length.
- **`Response`**: `.status_code` (`.status` alias), `.ok`, `.reason`, `.headers`,
  `.content`, `.text`, `.json()`, `.url`, `.resumed`, `.cookies`, `.raise_for_status()`.
- **Cookies** (dict-like, like curl_cffi / httpx): `s.cookies["k"]`,
  `s.cookies.get("k", domain=…, path=…)`, `.get_dict(domain=…, path=…)`,
  `.set("k", "v", domain=…, path=…)`, `.delete("k", domain=…, path=…)`,
  `.clear(domain=…, path=…)`, `.update()`, `.keys()/.values()/.items()`,
  `"k" in s.cookies`, `dict(s.cookies)`; `r.cookies` for what a response set.
  For durable process/Redis persistence, store `s.cookies.get_records()` and
  restore each record with `s.cookies.set_record(**record)`. This preserves
  complete scope and policy attributes; `get_dict()` is only a flattened view.
- **`never_safari.HTTPError`**, `Session` (alias of `Client`).

The network call releases the GIL, so many Python threads can crawl concurrently.

## Async — `AsyncSession` (like curl_cffi)

```python
import asyncio, never_safari

async def main():
    async with never_safari.AsyncSession() as s:      # max_workers= bounds concurrency
        r = await s.get("https://example.com", params={"q": "1"})
        r = await s.post(url, json={"a": 1})
        results = await asyncio.gather(*[s.get(u) for u in urls])   # runs concurrently

asyncio.run(main())
```

Same verbs, kwargs, `Response` and `.cookies` as the sync `Session`; every verb is
a coroutine. It runs the (GIL-releasing) sync core in a bounded thread pool, so
requests run genuinely in parallel. `AsyncClient` is an alias.

See the [project README](https://github.com/neverl805/never_safari) for the full
fingerprint details and scope.

