Metadata-Version: 2.4
Name: rex-tls
Version: 2.2.1
Classifier: Development Status :: 3 - Alpha
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Rust
Classifier: Topic :: Internet :: WWW/HTTP
Requires-Dist: certifi>=2024
Requires-Dist: maturin>=1.9,<2 ; extra == 'dev'
Requires-Dist: pytest>=8,<10 ; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24,<2 ; extra == 'dev'
Requires-Dist: requests>=2.32,<3 ; extra == 'dev'
Requires-Dist: trustme>=1.2.1,<2 ; extra == 'dev'
Requires-Dist: websocket-client>=1.8,<2 ; extra == 'dev'
Requires-Dist: h2>=4.3,<4.4 ; python_full_version < '3.10' and extra == 'dev'
Requires-Dist: h2>=4.4,<5 ; python_full_version >= '3.10' and extra == 'dev'
Provides-Extra: dev
License-File: LICENSE
Summary: Python HTTP client with native Android Chrome and OkHttp TLS/HTTP2 profiles
Keywords: tls,http2,android,chrome,okhttp,boringssl
Home-Page: https://github.com/rex3129909440/rex-tls
Author: rex-tls contributors
License: MIT
Requires-Python: >=3.9
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Documentation, https://github.com/rex3129909440/rex-tls#readme
Project-URL: Homepage, https://github.com/rex3129909440/rex-tls
Project-URL: Repository, https://github.com/rex3129909440/rex-tls

# rex-tls

`rex-tls` is a Python HTTP client backed by a native Rust TLS and HTTP core. It
provides a requests-style API with transport profiles for Android Chrome and
OkHttp, persistent sessions, cookies, proxies, streaming I/O, asynchronous
requests, and bounded connection pools.

## Installation

```bash
python -m pip install rex-tls
```

Python 3.9 or newer is required. Supported platforms are:

- Windows x86-64
- Linux x86-64 with manylinux 2.28 or newer

## Transport profiles

| Profile | HTTP/1.1 | HTTP/2 | HTTP/3 |
|---|:---:|:---:|:---:|
| `chrome_android_149` | Yes | Yes | Yes |
| `chrome_android_150` | Yes | Yes | Yes |
| `okhttp_4.12` | Yes | Yes | No |
| `okhttp_5.4` | Yes | Yes | No |

`chrome_android` and `chrome_android_latest` are aliases for
`chrome_android_150`. `okhttp` and `okhttp_latest` are aliases for `okhttp_5.4`.
Use the versioned names when reproducibility matters.

## Quick start

```python
import rex_tls

response = rex_tls.get(
    "https://example.com/",
    profile="chrome_android_150",
    params={"page": 1},
    timeout=20,
)

response.raise_for_status()
print(response.status_code)
print(response.http_version)
print(response.text)
```

JSON and form requests use familiar keyword arguments:

```python
response = rex_tls.post(
    "https://api.example.com/items",
    profile="okhttp_5.4",
    json={"name": "example", "enabled": True},
)

response = rex_tls.post(
    "https://api.example.com/form",
    profile="okhttp_4.12",
    data={"name": "example"},
)
```

## Persistent sessions

A `Session` keeps cookies, connections, and TLS session state across requests.

```python
from rex_tls import Session

with Session(
    profile="chrome_android_150",
    timeout=20,
) as session:
    session.headers.update({"accept": "application/json"})
    session.cookies.set("locale", "en-US")

    first = session.get("https://example.com/api/profile")
    second = session.get("https://example.com/api/settings")

    print(second.connection_reused)
    print(session.cookies.get_dict())
```

Per-request cookies are supported and do not modify the session jar:

```python
response = session.get(
    "https://example.com/api/items",
    params={"page": 2},
    cookies={"request-only": "value"},
    headers={"accept": "application/json"},
)
```

## Cookies

`Session.cookies` provides the commonly used requests-style CookieJar methods:

```python
session.cookies.update({"theme": "dark"})
session.cookies.set(
    "api-token",
    "value",
    domain="api.example.com",
    path="/v1",
    secure=True,
)

print(session.cookies.get("theme"))
print(session.cookies.get_dict())
print(session.cookies.items())

session.cookies.clear(domain="api.example.com", path="/v1", name="api-token")
```

Supported helpers include `set()`, `set_cookie()`, `get()`, `get_dict()`,
`update()`, `clear()`, `keys()`, `values()`, `items()`, `list_domains()`, and
`list_paths()`. Response cookies are validated against domain, path, Secure,
expiry, IP-address, and public-suffix rules before they enter the jar.

## Proxies

Use `proxy=` as a single proxy for all supported routes:

```python
proxy = "http://username:password@proxy.example:8080"

with Session("okhttp_5.4", proxy=proxy) as session:
    response = session.get("https://example.com/")
```

Or use a requests-style mapping:

```python
proxies = {
    "http": "http://proxy.example:8080",
    "https": "http://proxy.example:8080",
    "all": "http://fallback.example:8080",
    "no_proxy": ".internal.example,localhost,127.0.0.1",
}

with Session("chrome_android_149", proxies=proxies) as session:
    response = session.get("https://example.com/")
```

`Session.proxies` is mutable. A per-request `proxies=` mapping overrides the
session mapping. Set `trust_env=True` to read `HTTP_PROXY`, `HTTPS_PROXY`,
`ALL_PROXY`, and `NO_PROXY`.

Proxy URLs must use `http://`. HTTPS destinations use HTTP CONNECT. Basic proxy
credentials are supported; percent-encode reserved characters in usernames and
passwords. HTTPS proxies, SOCKS, PAC, NTLM/Digest, and MASQUE are not supported.

## HTTP version selection

The negotiated protocol is available as `response.http_version`.

Require an actual HTTP/2 connection with `http2=True`:

```python
with Session("chrome_android_150", http2=True) as session:
    response = session.get("https://example.com/")
    assert response.http_version == "HTTP/2"
```

`http2=True` preserves the profile ALPN list and rejects the response if the
server does not negotiate HTTP/2. It supports HTTPS only and is mutually
exclusive with HTTP/3 modes.

Chrome profiles support the following `http3` values:

| Value | Behavior |
|---|---|
| `off` | Disable HTTP/3. This is the default. |
| `auto` | Enable the Chrome HTTP/3 path with HTTP/2 or HTTP/1.1 fallback. |
| `only` | Require HTTP/3 and fail if it cannot be used. |

```python
with Session("chrome_android_150", http3="auto") as session:
    response = session.get("https://example.com/")

with Session("chrome_android_149", http3="only") as session:
    response = session.get("https://example.com/")
    assert response.http_version == "HTTP/3"
```

HTTP/3 requires HTTPS and a versioned Chrome profile. OkHttp profiles do not
enable HTTP/3. Conventional HTTP proxies cannot carry the QUIC path, so
`http3="only"` is rejected when such a proxy is selected.

## Request headers

Headers may be supplied as a mapping or as ordered `(name, value)` pairs. The
native core applies the selected profile's protocol-specific ordering and casing
rules before transmission.

If both session and request headers are empty, the profile's default request
headers are used. A non-empty caller header set is sent without adding unrelated
profile defaults. Protocol-required fields and fields derived from cookies or the
request body may still be generated.

HTTP/1.1 preserves the profile's observable field-name casing. HTTP/2 and HTTP/3
field names are lowercase as required by those protocols. Hop-by-hop HTTP/1.1
fields such as `Connection` are rejected for HTTP/2 and HTTP/3 instead of being
silently removed.

## Streaming downloads

Use a context manager so the connection is released when the body reaches EOF or
the response is closed:

```python
from rex_tls import Session

with Session("chrome_android_150") as session:
    with session.get("https://example.com/large.bin", stream=True) as response:
        response.raise_for_status()
        with open("large.bin", "wb") as output:
            for chunk in response.iter_content(64 * 1024):
                output.write(chunk)
```

`iter_lines()`, `raw.read()`, and `raw.readinto()` are also available. Set
`decode_content=False` to receive the compressed response body without automatic
content decoding.

## Streaming uploads and multipart files

File objects and byte iterables are uploaded incrementally:

```python
with Session("okhttp_5.4") as session:
    with open("large.bin", "rb") as source:
        response = session.post(
            "https://example.com/upload",
            content=source,
        )

    with open("image.png", "rb") as source:
        response = session.post(
            "https://example.com/form",
            data={"title": "example"},
            files={"file": ("image.png", source, "image/png")},
        )
```

Seekable upload sources can be replayed across 307/308 redirects. A one-shot
source raises `UnrewindableBodyError` if a redirect requires replay.

## Async API

`AsyncSession` provides an asyncio interface without blocking the event loop:

```python
import asyncio
from rex_tls import AsyncSession

async def main() -> None:
    async with AsyncSession(
        "okhttp_5.4",
        max_concurrency=8,
        http2=True,
    ) as session:
        urls = [f"https://example.com/items/{item}" for item in range(10)]
        responses = await asyncio.gather(*(session.get(url) for url in urls))
        print([response.status_code for response in responses])

asyncio.run(main())
```

Async streaming uses `aiter_content()` and `aiter_lines()`:

```python
from rex_tls import AsyncSession

async def stream_events(session: AsyncSession) -> None:
    response = await session.get("https://example.com/events", stream=True)
    async with response:
        async for line in response.aiter_lines():
            print(line)
```

## Bounded session pools

`SessionPool` and `AsyncSessionPool` create multiple independent native sessions
with a fixed concurrency limit.

```python
from concurrent.futures import ThreadPoolExecutor
from rex_tls import SessionPool

urls = [f"https://example.com/items/{item}" for item in range(20)]

with SessionPool(
    "okhttp_4.12",
    max_connections=8,
    session_mode="shared",
) as pool:
    pool.headers["accept"] = "application/json"
    with ThreadPoolExecutor(max_workers=16) as executor:
        responses = list(executor.map(pool.get, urls))
```

Pool session modes:

- `shared`: members share headers, proxy configuration, and one thread-safe
  CookieJar. Connections remain independent.
- `isolated`: each member owns independent headers, proxy configuration, cookies,
  connections, and TLS state.

Lease a specific member when multiple requests must keep the same isolated state:

```python
from rex_tls import SessionPool

with SessionPool(
    "okhttp_5.4",
    max_connections=4,
    session_mode="isolated",
) as pool:
    with pool.acquire(timeout=1) as account:
        account.headers["authorization"] = "Bearer example-token"
        account.cookies.set("account", "one")
        account.get("https://example.com/step-1")
        account.get("https://example.com/step-2")
```

Use `pool_timeout=` to limit how long a request waits for a pool member. The
ordinary `timeout=` argument continues to control the network request.

## Response API

Common response attributes and methods include:

- `status_code`, `reason`, `url`, and `http_version`
- `headers`, including `get_all()` and the ordered `raw` view
- `content`, `text`, and `json()`
- `ok` and `raise_for_status()`
- `history`
- `elapsed_seconds`
- `local_address` and `remote_address`
- `connection_reused` and `tls_session_reused`
- `iter_content()`, `iter_lines()`, `close()`, and `aclose()`

## Requests compatibility

The API directly supports common requests-style arguments and attributes:

- HTTP method helpers and `Session`
- `params`, `headers`, `data`, `json`, `files`, and `cookies`
- `timeout`, `verify`, `proxy`, and `proxies`
- `Session.headers`, `Session.proxies`, `Session.cookies`, and `trust_env`
- `stream=True`, `iter_content()`, `iter_lines()`, and raw reads
- per-request `allow_redirects`
- `.content`, `.text`, `.json()`, and `.raise_for_status()`

It is not a drop-in replacement for every requests extension point. `auth`,
`hooks`, `adapters`, `mount`, and custom RequestsCookieJar policy objects are not
implemented.

## TLS verification

Certificate verification is enabled by default and uses the installed `certifi`
CA bundle.

```python
# Default CA bundle
Session("chrome_android_150", verify=True)

# Custom CA file
Session("chrome_android_150", verify="/path/to/private-ca.pem")

# Disable verification explicitly
Session("chrome_android_150", verify=False)
```

Disabling verification removes server identity protection and should only be used
in controlled test environments.

## Error handling

```python
import rex_tls

try:
    response = rex_tls.get(
        "https://example.com/",
        profile="chrome_android_150",
        timeout=10,
    )
    response.raise_for_status()
except rex_tls.HTTPError as exc:
    print(f"HTTP error: {exc}")
except rex_tls.RequestError as exc:
    print(f"Request failed: {exc}")
except rex_tls.MobileTLSError as exc:
    print(f"Native client error: {exc}")
except (TypeError, ValueError) as exc:
    print(f"Invalid configuration: {exc}")
```

Additional exceptions include `CookieConflictError`, `ContentDecodingError`,
`InvalidHeader`, `InvalidURL`, `SessionClosedError`, `StreamClosedError`,
`StreamConsumedError`, and `UnrewindableBodyError`.

## Runtime information

```python
import rex_tls

print(rex_tls.__version__)
print(rex_tls.profiles())
print(dict(rex_tls.native_versions()))
print(rex_tls.profile_info("chrome_android_150"))
```

## License

MIT

