Metadata-Version: 2.4
Name: RedCum
Version: 0.0.2
Summary: Async HTTP client for Python with native libcurl performance.
Keywords: asyncio,http,client,http-client,curl,libcurl,native
Author-Email: ricl <rick@gmail.com>
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
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 :: 3.15
Classifier: Programming Language :: C++
Classifier: Framework :: AsyncIO
Classifier: Topic :: Internet :: WWW/HTTP
Project-URL: Source, https://github.com/rick/RedCum
Project-URL: Tracker, https://github.com/rick/RedCum/issues
Requires-Python: >=3.10
Requires-Dist: trustifi
Provides-Extra: test
Requires-Dist: pytest-asyncio; extra == "test"
Requires-Dist: waitress; extra == "test"
Requires-Dist: httpbin; extra == "test"
Description-Content-Type: text/markdown

# RedCum

Async HTTP client for Python with native libcurl performance, extended with:

1. **A synchronous API** (`RedCum.sync`) — no `async`/`await` required.
2. **`impersonate=` support** wired through to
   [libcurl-impersonate](https://github.com/lexiforest/curl-impersonate),
   with an auto-detecting fetch script (`fetch_impersonate.sh`) so getting
   it built is one command, not a manual per-architecture hunt — see
   [BUILDING_IMPERSONATE.md](BUILDING_IMPERSONATE.md).
3. Every libcurl failure raised as a specific, catchable Python exception —
   this was already true in upstream RedCum and is unchanged/preserved here.
4. A fix for a real crash-on-shutdown bug found while testing this fork
   (see "What changed and why" below).

Everything upstream RedCum does (async `Client`, streaming, HTTP/1.1/2/3,
cookies, connection pooling, `CurlURL`, ...) still works exactly as before —
nothing was removed, only added to.

## Quick start (sync API)

```python
import RedCum.sync as curl

resp = curl.get("https://target.com", impersonate="chrome110")
resp2 = curl.get("https://target.com", impersonate="safari18_0_ios")

print(resp.status_code, resp.text[:200])
```

No client setup, no event loop, no `await`. Connections are still pooled
under the hood (a background thread owns one persistent event loop + one
`RedCum.Client`), so repeated calls to `curl.get(...)` aren't reconnecting
from scratch each time.

`impersonate` requires RedCum to be built against libcurl-impersonate.
`./publish.sh --build-only --auto-impersonate` handles this end-to-end —
auto-detects your architecture, downloads the matching library, and builds
a working wheel — verified for real, not just documented (see
[BUILDING_IMPERSONATE.md](BUILDING_IMPERSONATE.md), which walks through
the exact process that was used to confirm this: downloaded the actual
release archive, linked RedCum against it, and confirmed real HTTP requests
with `impersonate="chrome110"` and `impersonate="safari18_0_ios"` both
return `200`. On a stock-libcurl build (what ships in this zip by default),
passing `impersonate=` raises a clear `RuntimeError` explaining exactly
what to do — it never silently ignores the argument or crashes.

```python
import RedCum
print(RedCum.has_impersonate())  # False until built per BUILDING_IMPERSONATE.md
```

### Session — for anything beyond one-off calls

```python
import RedCum.sync as curl

with curl.Session(impersonate="chrome110", timeout=15) as s:
    for url in urls:
        r = s.get(url)
        print(r.status_code)
```

Use `Session` when you want:
- Shared headers/cookies/timeout across many calls
- A single `impersonate` profile applied to every request from that session
- Deterministic cleanup (`with ... :` or explicit `.close()`)
- To make concurrent calls from multiple threads against one connection pool
  (a `Session` is thread-safe — see `tests/test_sync_api.py::test_concurrent_calls_from_multiple_threads`)

### Every request parameter is still there

`curl.get`, `.post`, `.put`, `.patch`, `.delete`, `.head`, `.options`, and
`.request` all accept the same arguments as `RedCum.Client`'s async methods —
`params`, `json`, `data`, `files`, `headers`, `cookies`, `timeout`,
`allow_redirects`, `proxy_url`, `auth`, `verify`, `cert`,
`stream_callback`, `progress_callback`, `verbose` — plus the new
`impersonate` / `impersonate_default_headers`.

```python
resp = curl.post(
    "https://api.internal.example.com/upload",
    files={"file": ("report.pdf", open("report.pdf", "rb"))},
    headers={"Authorization": "Bearer ..."},
    impersonate="chrome110",
    timeout=30,
)
```

## Error handling — the whole point of "simple"

**Every connection-level failure raises.** DNS failure, TLS handshake
failure, connection refused, timeout — you never get back a `Response` that
silently has `status_code == -1`. This is different from upstream RedCum's
default (where that's opt-in via `raise_for_status=True`); for a "simple"
API the sync layer here always raises on curl-level failure, since a caller
who didn't opt into error-handling complexity shouldn't have to check for a
magic `-1` sentinel.

```python
import RedCum.sync as curl
import RedCum.exceptions as exc

try:
    curl.get("https://this-domain-does-not-exist.invalid")
except exc.CouldntResolveHostError as e:
    print("DNS lookup failed:", e)
except exc.CurlError as e:
    # catches every other libcurl failure: timeouts, TLS errors,
    # connection refused, too many redirects, etc.
    print(f"Request failed ({e.code}):", e)
```

**HTTP 4xx/5xx status codes stay opt-in**, same as `requests`/`httpx`:

```python
resp = curl.get("https://api.example.com/missing")
print(resp.status_code)  # 404, no exception

# opt in per-call:
resp.raise_for_status()  # raises HTTPError now

# or opt in for a whole session:
with curl.Session(raise_for_status=True) as s:
    s.get("https://api.example.com/missing")  # raises HTTPError immediately
```

Every libcurl `CURLcode` has its own exception class (`CouldntResolveHostError`,
`OperationTimedoutError`, `SslConnectErrorError`, `PeerFailedVerificationError`,
...) — see `RedCum/exceptions/__init__.py`. This was already the case in
upstream RedCum; this fork doesn't touch that mapping, just makes sure the
sync layer actually surfaces it instead of swallowing it.

