Metadata-Version: 2.4
Name: fizzpy
Version: 0.4.0
Summary: A low-level TLS 1.3 client toolkit for Python, built on Facebook's Fizz
Author-Email: Xevion <xevion@xevion.dev>
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: POSIX :: Linux
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: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Security :: Cryptography
Classifier: Typing :: Typed
Project-URL: Repository, https://github.com/Xevion/fizz-py
Project-URL: Issues, https://github.com/Xevion/fizz-py/issues
Requires-Python: >=3.10
Requires-Dist: certifi
Provides-Extra: test
Requires-Dist: pytest>=8; extra == "test"
Description-Content-Type: text/markdown

# fizzpy

A small TLS 1.3 HTTP client for Python, built on Facebook's
[Fizz](https://github.com/facebookincubator/fizz) (C++) via pybind11.

By default it offers a **post-quantum key exchange** — the standardized hybrid
`X25519MLKEM768` group (codepoint 4588). An ordinary `GET` negotiates a hybrid
ML-KEM handshake against servers that support it (Cloudflare, Google) and falls
back to classical X25519 against those that don't.

> TLS 1.3 only, HTTP/1.1 only. This is a focused toolkit / learning project,
> not a drop-in `requests` replacement. See [Status](#status).

![A fizzpy ClientHello in Wireshark](https://raw.githubusercontent.com/Xevion/fizz-py/master/docs/pq-handshake.png)

*A fizzpy request captured in Wireshark: the TLS 1.3 ClientHello offers the
post-quantum `X25519MLKEM768` group (`0x11ec`), and a custom marker extension
(`0xFE5A`, [see below](#custom-clienthello-extensions)) makes it unmistakably
ours — generated by [`examples/pq_marker.py`](examples/pq_marker.py).*

## Install

```sh
pip install fizzpy
```

The Linux wheels are self-contained: folly, Fizz, and liboqs are statically
linked into the extension, so a fresh `pip install` has working post-quantum
TLS with no system packages to add. `certifi` (pulled in automatically) supplies
the default trust store.

Wheels are produced by CI (`manylinux_2_28`, x86_64, CPython 3.10–3.14); if one
isn't available for your platform yet, build from source — see
[BUILDING.md](BUILDING.md).

## Usage

Synchronous:

```python
import fizzpy

r = fizzpy.get("https://www.cloudflare.com")
print(r.status_code)               # 200
print(r.headers["content-type"])   # text/html; charset=UTF-8
print(r.text[:64])

# The negotiated TLS 1.3 parameters are attached to every response:
print(r.tls)
# {'version': 'TLSv1.3', 'cipher': 'TLS_AES_128_GCM_SHA256',
#  'group': 'X25519MLKEM768', 'group_code': 4588,
#  'alpn': 'http/1.1', 'sni': 'www.cloudflare.com', 'peer_cert': '...'}
```

Asynchronous (the same C++ core, resolved on the running event loop):

```python
import asyncio
from fizzpy.aio import AsyncClient

async def main():
    async with AsyncClient() as client:
        r = await client.get("https://www.google.com")
        print(r.status_code, r.tls["group"], r.tls["group_code"])

asyncio.run(main())
```

### Choosing the key exchange

The default offers `[x25519_mlkem768, x25519]`, mirroring how Chrome and Firefox
send both key shares. Override it per client:

```python
import fizzpy
from fizzpy import NamedGroup

# Force post-quantum only (handshake fails if the server lacks ML-KEM):
pq = fizzpy.Client(groups=[NamedGroup.x25519_mlkem768])

# Classical only:
classical = fizzpy.Client(groups=[NamedGroup.x25519])
```

### Custom ClientHello extensions

fizzpy surfaces Fizz's low-level extension hook, so you can append arbitrary
extensions to the TLS 1.3 ClientHello. Each is an opaque `(type, data)` pair;
fizzpy rejects types it manages itself (key_share, supported_versions, …) and
out-of-range or duplicate types.

```python
import fizzpy

client = fizzpy.Client(
    extensions=[fizzpy.Extension(0xFE5A, b"hello from fizzpy")],
)
r = client.get("https://blog.cloudflare.com")
print(r.tls["group"])   # still negotiates X25519MLKEM768
```

The extension is sent verbatim in the ClientHello — see it on the wire with
[`examples/pq_marker.py`](examples/pq_marker.py) and the screenshot above.

### Certificate verification

Certificate chains are verified against the [certifi](https://github.com/certifi/python-certifi)
CA bundle and the hostname is checked against the certificate's SAN by default.
Point at a custom CA, or disable verification entirely:

```python
fizzpy.Client(cafile="/path/to/ca.pem")   # trust a specific CA
fizzpy.Client(verify=False)               # accept any certificate (insecure)
```

## Status

What works today:

- TLS 1.3 handshake with classical or post-quantum (ML-KEM) key exchange
- `GET`/`POST`/`HEAD`/`PUT`/`DELETE`, sync and async
- HTTP/1.1 response framing (content-length, chunked, gzip/deflate)
- Connection keep-alive with a per-host pool; automatic redirect following
- Chain + hostname certificate verification, certifi default + custom CA trust

Current limitations:

- **TLS 1.3 only** (a Fizz constraint) — it cannot talk to TLS 1.2-only servers
- HTTP/1.1 only (no HTTP/2)

## Building

The post-quantum handshake requires a Fizz built against
[liboqs](https://github.com/open-quantum-safe/liboqs). The CI wheels build the
whole folly + Fizz + liboqs tree from source inside `manylinux_2_28`; see
[BUILDING.md](BUILDING.md) for that recipe and for local development builds.
