fizzpy 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,16 @@
1
+ # Manylinux base image with the folly + fizz + liboqs tree prebuilt, so wheel
2
+ # builds skip the ~40-min source build and just compile _core against it. Rebuilt
3
+ # by .github/workflows/manylinux-image.yml when build_fizz_deps.py or FIZZ_TAG
4
+ # changes. The dep prefixes land in $FIZZPY_BUILD_INFO/prefix.txt, which CMake
5
+ # reads at configure time (see CMakeLists.txt).
6
+ ARG MANYLINUX_IMAGE=quay.io/pypa/manylinux_2_28_x86_64:latest
7
+ FROM ${MANYLINUX_IMAGE}
8
+
9
+ ARG FIZZ_TAG=v2026.06.08.00
10
+ ENV FIZZ_TAG=${FIZZ_TAG} \
11
+ GETDEPS_SCRATCH=/opt/fizz-deps \
12
+ FIZZPY_BUILD_INFO=/opt/fizzpy-build \
13
+ FIZZ_SRC=/opt/fizz-src
14
+
15
+ COPY scripts/build_fizz_deps.py /opt/build_fizz_deps.py
16
+ RUN python3 /opt/build_fizz_deps.py
@@ -0,0 +1,38 @@
1
+ name: Check
2
+
3
+ on:
4
+ push:
5
+ branches: [master]
6
+ tags-ignore: ["v*"]
7
+ pull_request:
8
+ workflow_dispatch:
9
+
10
+ concurrency:
11
+ group: ${{ github.workflow }}-${{ github.ref }}
12
+ cancel-in-progress: true
13
+
14
+ jobs:
15
+ quality:
16
+ name: Quality
17
+ runs-on: ubuntu-latest
18
+ timeout-minutes: 10
19
+ steps:
20
+ - uses: actions/checkout@v6
21
+
22
+ - name: Install uv
23
+ uses: astral-sh/setup-uv@v8.2.0
24
+ with:
25
+ enable-cache: true
26
+
27
+ # _core.pyi lets ruff and basedpyright run without the native build.
28
+ - name: Sync toolchain
29
+ run: uv sync --no-install-project --group dev
30
+
31
+ - name: Lint
32
+ run: uv run --no-sync ruff check --output-format=github .
33
+
34
+ - name: Format
35
+ run: uv run --no-sync ruff format --check .
36
+
37
+ - name: Type check
38
+ run: uv run --no-sync basedpyright
@@ -0,0 +1,31 @@
1
+ name: Publish
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ jobs:
8
+ # Reuse the wheel/sdist build so a tag produces exactly the artifacts CI
9
+ # already validates on every push. packages: write lets the deps_image job
10
+ # rebuild the image if a tag introduces a deps change with no prior push.
11
+ build:
12
+ permissions:
13
+ contents: read
14
+ packages: write
15
+ uses: ./.github/workflows/wheels.yml
16
+
17
+ publish:
18
+ name: Publish to PyPI
19
+ needs: build
20
+ runs-on: ubuntu-latest
21
+ environment: pypi
22
+ # OIDC token for PyPI Trusted Publishing — no API token/secret required.
23
+ permissions:
24
+ id-token: write
25
+ steps:
26
+ - uses: actions/download-artifact@v8
27
+ with:
28
+ path: dist
29
+ merge-multiple: true
30
+
31
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,118 @@
1
+ name: Wheels
2
+
3
+ on:
4
+ push:
5
+ branches: [master]
6
+ tags: ["v*"]
7
+ pull_request:
8
+ workflow_dispatch:
9
+ workflow_call:
10
+
11
+ concurrency:
12
+ group: ${{ github.workflow }}-${{ github.ref }}
13
+ cancel-in-progress: true
14
+
15
+ jobs:
16
+ # The folly+fizz+liboqs tree is baked into a GHCR image tagged by the hash of
17
+ # its build inputs. This job rebuilds it only when that hash has no image yet
18
+ # (deps changed, or first run); otherwise it's a fast existence check. Wheels
19
+ # depend on it, so they never run against a missing image.
20
+ deps_image:
21
+ name: Deps image
22
+ runs-on: ubuntu-latest
23
+ timeout-minutes: 120
24
+ permissions:
25
+ contents: read
26
+ packages: write
27
+ outputs:
28
+ image: ${{ steps.meta.outputs.image }}
29
+ steps:
30
+ - uses: actions/checkout@v6
31
+
32
+ - name: Resolve image ref
33
+ id: meta
34
+ env:
35
+ OWNER: ${{ github.repository_owner }}
36
+ run: |
37
+ repo="ghcr.io/${OWNER,,}/fizz-py-deps"
38
+ tag="${{ hashFiles('scripts/build_fizz_deps.py', '.github/manylinux-deps.Dockerfile') }}"
39
+ echo "repo=$repo" >> "$GITHUB_OUTPUT"
40
+ echo "image=$repo:$tag" >> "$GITHUB_OUTPUT"
41
+
42
+ - name: Log in to GHCR
43
+ uses: docker/login-action@v4
44
+ with:
45
+ registry: ghcr.io
46
+ username: ${{ github.actor }}
47
+ password: ${{ secrets.GITHUB_TOKEN }}
48
+
49
+ - name: Check whether the image already exists
50
+ id: exists
51
+ run: |
52
+ if docker manifest inspect "${{ steps.meta.outputs.image }}" > /dev/null 2>&1; then
53
+ echo "found=true" >> "$GITHUB_OUTPUT"
54
+ else
55
+ echo "found=false" >> "$GITHUB_OUTPUT"
56
+ fi
57
+
58
+ - name: Build and push deps image
59
+ if: steps.exists.outputs.found == 'false'
60
+ uses: docker/build-push-action@v7
61
+ with:
62
+ context: .
63
+ file: .github/manylinux-deps.Dockerfile
64
+ push: true
65
+ tags: |
66
+ ${{ steps.meta.outputs.image }}
67
+ ${{ steps.meta.outputs.repo }}:latest
68
+
69
+ build_sdist:
70
+ name: Build sdist
71
+ runs-on: ubuntu-latest
72
+ timeout-minutes: 10
73
+ steps:
74
+ - uses: actions/checkout@v6
75
+ - run: pipx run build --sdist
76
+ - uses: actions/upload-artifact@v7
77
+ with:
78
+ name: sdist
79
+ path: dist/*.tar.gz
80
+
81
+ build_wheels:
82
+ # One job per interpreter, each compiling _core against the prebuilt deps
83
+ # image (minutes, not ~40 min). liboqs travels with fizz's getdeps manifest,
84
+ # giving real post-quantum (X25519MLKEM768) support.
85
+ name: Wheel ${{ matrix.python }}
86
+ needs: deps_image
87
+ runs-on: ubuntu-latest
88
+ timeout-minutes: 30
89
+ permissions:
90
+ contents: read
91
+ packages: read
92
+ strategy:
93
+ fail-fast: false
94
+ matrix:
95
+ python: [cp310, cp311, cp312, cp313, cp314]
96
+ steps:
97
+ - uses: actions/checkout@v6
98
+
99
+ # cibuildwheel pulls the deps image via docker; authenticate so the GHCR
100
+ # package is reachable.
101
+ - name: Log in to GHCR
102
+ uses: docker/login-action@v4
103
+ with:
104
+ registry: ghcr.io
105
+ username: ${{ github.actor }}
106
+ password: ${{ secrets.GITHUB_TOKEN }}
107
+
108
+ - name: Build wheel
109
+ uses: pypa/cibuildwheel@v4.1.0
110
+ env:
111
+ CIBW_BUILD: ${{ matrix.python }}-*
112
+ CIBW_MANYLINUX_X86_64_IMAGE: ${{ needs.deps_image.outputs.image }}
113
+
114
+ - uses: actions/upload-artifact@v7
115
+ with:
116
+ name: wheels-${{ matrix.python }}
117
+ path: wheelhouse/*.whl
118
+ if-no-files-found: error
@@ -0,0 +1,46 @@
1
+ build/
2
+ CMakeFiles/
3
+ dist/
4
+ .dev/
5
+ _skbuild/
6
+ wheelhouse/
7
+ *.egg-info/
8
+ __pycache__/
9
+ *.pyc
10
+ .pytest_cache/
11
+ .mypy_cache/
12
+ .ruff_cache/
13
+ .venv/
14
+
15
+ # Prerequisites
16
+ *.d
17
+
18
+ # Compiled Object files
19
+ *.slo
20
+ *.lo
21
+ *.o
22
+ *.obj
23
+
24
+ # Precompiled Headers
25
+ *.gch
26
+ *.pch
27
+
28
+ # Compiled Dynamic libraries
29
+ *.so
30
+ *.dylib
31
+ *.dll
32
+
33
+ # Fortran module files
34
+ *.mod
35
+ *.smod
36
+
37
+ # Compiled Static libraries
38
+ *.lai
39
+ *.la
40
+ *.a
41
+ *.lib
42
+
43
+ # Executables
44
+ *.exe
45
+ *.out
46
+ *.app
@@ -0,0 +1,19 @@
1
+ {
2
+ "configurations": [
3
+ {
4
+ "name": "Linux",
5
+ "includePath": [
6
+ "${workspaceFolder}/**",
7
+ "${workspaceFolder}/build/vcpkg_installed/**",
8
+ "/usr/include/python3.10/**"
9
+ ],
10
+ "defines": [],
11
+ "compilerPath": "/usr/bin/clang",
12
+ "cStandard": "c17",
13
+ "cppStandard": "c++20",
14
+ "intelliSenseMode": "linux-clang-x64",
15
+ "configurationProvider": "ms-vscode.cmake-tools"
16
+ }
17
+ ],
18
+ "version": 4
19
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "files.associations": {
3
+ "*.ipp": "cpp",
4
+ "*.tcc": "cpp",
5
+ "optional": "cpp",
6
+ "system_error": "cpp",
7
+ "regex": "cpp"
8
+ },
9
+ "cSpell.words": [
10
+ "fizzpy"
11
+ ]
12
+ }
@@ -0,0 +1,57 @@
1
+ # Building fizzpy
2
+
3
+ The `_core` extension links Facebook's Fizz (C++ TLS 1.3). The post-quantum
4
+ handshake additionally requires a Fizz built against
5
+ [liboqs](https://github.com/open-quantum-safe/liboqs) — the Homebrew bottle and
6
+ vcpkg port both ship `FIZZ_HAVE_OQS=0`, so they negotiate classical groups only.
7
+
8
+ There are two build paths, for two different purposes:
9
+
10
+ | | Local dev (`just build`) | CI wheels (`scripts/build_fizz_deps.py`) |
11
+ | --- | --- | --- |
12
+ | Fizz/liboqs source | Homebrew + from-source Fizz | getdeps, all from source |
13
+ | Linkage | static Fizz, shared liboqs via rpath | fully static-PIC, self-contained `.so` |
14
+ | Where | your machine | `manylinux_2_28` container |
15
+ | Optimized for | fast iteration | deterministic, portable distribution |
16
+
17
+ ## Local development (`just`)
18
+
19
+ ```sh
20
+ brew install fizz liboqs # fizz pulls folly, boost, glog, gflags, libsodium, double-conversion
21
+ pip install -e .[dev] --no-build-isolation # or: pip install scikit-build-core pybind11 pybind11-stubgen ruff pytest
22
+
23
+ just build # build OQS Fizz if needed (cached), then editable-install _core
24
+ just test # offline test suite; `just test ""` includes live network tests
25
+ just check # ruff lint
26
+ just fmt # ruff format + clang-format _core.cpp
27
+ ```
28
+
29
+ `just build` runs `scripts/build_fizz.py ensure`, which compiles a liboqs-enabled
30
+ Fizz into the gitignored `.dev/fizz-prefix` once (subsequent runs are a no-op;
31
+ `python scripts/build_fizz.py ensure --force` rebuilds), then editable-installs
32
+ `_core` with `find_package(fizz)` pointed at that prefix.
33
+
34
+ - **C++20 is required** — current Folly (2026.06) dropped C++17.
35
+ - Fizz and Folly are versioned together. The tag in `scripts/build_fizz.py` must
36
+ match the Homebrew Folly bottle, or the ABI won't match.
37
+ - `find_package(liboqs CONFIG)` inside Fizz auto-sets `FIZZ_HAVE_OQS` and links
38
+ `OQS::oqs`. `BUILD_EXAMPLES=OFF` is required: the bundled FizzTool fails to
39
+ compile under OpenSSL 3.6 (`-Werror` on the deprecated `EVP_PKEY_cmp`); the
40
+ library itself is fine.
41
+
42
+ ## Portable wheels (CI)
43
+
44
+ `scripts/build_fizz_deps.py` is cibuildwheel's `before-all`. It builds the whole
45
+ folly + Fizz + liboqs tree once per `manylinux_2_28` container as static-PIC
46
+ archives, which `_core` links in wholesale — a single self-contained extension
47
+ with exactly one copy of folly. (Using getdeps `--shared-lib` instead would bake
48
+ folly into `libfizz.so` *and* drag it into `_core` again, double-registering
49
+ folly's gflags globals and aborting at import.) The build settings live in
50
+ `[tool.cibuildwheel]` in `pyproject.toml`.
51
+
52
+ ## Verify
53
+
54
+ ```sh
55
+ python -c "import fizzpy; print(fizzpy.get('https://www.cloudflare.com').tls)"
56
+ # group should be 'X25519MLKEM768', group_code 4588
57
+ ```
@@ -0,0 +1,88 @@
1
+ cmake_minimum_required(VERSION 3.15...3.27)
2
+ set(CMAKE_CXX_STANDARD 20)
3
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
4
+
5
+ project(
6
+ ${SKBUILD_PROJECT_NAME}
7
+ VERSION ${SKBUILD_PROJECT_VERSION}
8
+ LANGUAGES CXX)
9
+
10
+ # Only Linux x86_64 wheels are published. Stop here before the (~40 min) folly +
11
+ # fizz + liboqs source build that an sdist install would otherwise attempt and
12
+ # fail on macOS/Windows.
13
+ if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux")
14
+ message(
15
+ FATAL_ERROR
16
+ "fizzpy publishes prebuilt Linux x86_64 wheels only; ${CMAKE_SYSTEM_NAME} "
17
+ "is not yet supported. Building from source needs the Fizz toolchain — see "
18
+ "BUILDING.md (https://github.com/Xevion/fizz-py/blob/master/BUILDING.md).")
19
+ endif()
20
+
21
+ # Fizz's CMake config pulls in its bundled FindSodium.cmake. Homebrew installs
22
+ # it under libexec/cmake; make that discoverable for local (non-vcpkg) builds.
23
+ # Override with -DFIZZ_CMAKE_MODULE_DIR=... if your install differs.
24
+ set(FIZZ_CMAKE_MODULE_DIR "/home/linuxbrew/.linuxbrew/opt/fizz/libexec/cmake"
25
+ CACHE PATH "Directory containing Fizz's bundled CMake find-modules")
26
+ if(EXISTS "${FIZZ_CMAKE_MODULE_DIR}")
27
+ list(APPEND CMAKE_MODULE_PATH "${FIZZ_CMAKE_MODULE_DIR}")
28
+ endif()
29
+
30
+ if(DEFINED ENV{VCPKG_ROOT} AND NOT DEFINED CMAKE_TOOLCHAIN_FILE)
31
+ set(CMAKE_TOOLCHAIN_FILE "$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake"
32
+ CACHE STRING "")
33
+ endif()
34
+
35
+ message(STATUS "Using compiler: ${CMAKE_CXX_COMPILER}")
36
+
37
+ # Portable-wheel builds: scripts/build_fizz_deps.py (cibuildwheel before-all)
38
+ # writes the getdeps install prefixes and fizz's find-module dir here. Read them
39
+ # at configure time rather than as a CIBW environment substitution, which would
40
+ # evaluate before before-all has produced the files. Skipped for local builds,
41
+ # which pass -DCMAKE_PREFIX_PATH directly (see BUILDING.md).
42
+ if(DEFINED ENV{FIZZPY_BUILD_INFO})
43
+ set(_info "$ENV{FIZZPY_BUILD_INFO}")
44
+ if(EXISTS "${_info}/prefix.txt")
45
+ file(STRINGS "${_info}/prefix.txt" _fizz_prefix)
46
+ list(APPEND CMAKE_PREFIX_PATH ${_fizz_prefix})
47
+ endif()
48
+ if(EXISTS "${_info}/module_dir.txt")
49
+ file(STRINGS "${_info}/module_dir.txt" _fizz_modules)
50
+ list(APPEND CMAKE_MODULE_PATH ${_fizz_modules})
51
+ endif()
52
+ endif()
53
+
54
+ # pybind11 is pip-installed (locally, or by before-build in wheel builds); add its
55
+ # CMake config so find_package locates it without a CIBW env substitution.
56
+ # Development.Module must be in this first find_package: pybind11's NewTools skips
57
+ # its own Python discovery once Python_FOUND is set, so python_add_library (from
58
+ # Development.Module) would otherwise never be defined.
59
+ find_package(Python COMPONENTS Interpreter Development.Module QUIET)
60
+ if(Python_Interpreter_FOUND)
61
+ execute_process(
62
+ COMMAND "${Python_EXECUTABLE}" -m pybind11 --cmakedir
63
+ OUTPUT_VARIABLE _pybind11_cmakedir
64
+ OUTPUT_STRIP_TRAILING_WHITESPACE
65
+ ERROR_QUIET)
66
+ if(_pybind11_cmakedir)
67
+ list(APPEND CMAKE_PREFIX_PATH "${_pybind11_cmakedir}")
68
+ endif()
69
+ endif()
70
+
71
+ set(PYBIND11_NEWPYTHON ON)
72
+ find_package(pybind11 CONFIG REQUIRED)
73
+ find_package(fizz CONFIG REQUIRED)
74
+
75
+ pybind11_add_module(_core _core.cpp)
76
+ target_link_libraries(_core PRIVATE fizz::fizz)
77
+
78
+ # _core.cpp drives connections on a folly::ScopedEventBaseThread, but fizz itself
79
+ # never uses that type — so when folly is a static archive (e.g. getdeps
80
+ # --shared-lib portable wheels), its object never makes it into libfizz and the
81
+ # symbol is undefined at import. Link the component directly when folly exposes
82
+ # it as a discrete target; classic shared-folly builds (Homebrew) export it from
83
+ # libfolly already and skip this.
84
+ if(TARGET Folly::folly_io_async_scoped_event_base_thread)
85
+ target_link_libraries(_core PRIVATE Folly::folly_io_async_scoped_event_base_thread)
86
+ endif()
87
+
88
+ install(TARGETS _core LIBRARY DESTINATION fizzpy)
fizzpy-0.3.0/Justfile ADDED
@@ -0,0 +1,35 @@
1
+ # fizzpy dev tasks. `just build` is the fast local loop (editable _core against
2
+ # a liboqs-enabled Fizz); CI wheels are built separately via scripts/build_fizz_deps.py.
3
+
4
+ _default:
5
+ @just --list
6
+
7
+ # Build the OQS Fizz prefix if needed, then editable-install _core against it.
8
+ build:
9
+ python scripts/build_fizz.py ensure
10
+ pip install -e . --no-build-isolation --config-settings=build-dir=.dev/build-editable --config-settings=cmake.define.CMAKE_PREFIX_PATH="$(python scripts/build_fizz.py prefix-path)"
11
+
12
+ # Build a non-editable wheel and force-reinstall it (full install, not the editable loop).
13
+ wheel:
14
+ python -m build
15
+ pip install ./dist/*.whl --force-reinstall
16
+
17
+ # Run the test suite. Defaults to the offline, deterministic subset; pass args to
18
+ # override, e.g. `just test ""` for the full suite (needs network).
19
+ test *args="-m 'not network'":
20
+ pytest {{args}}
21
+
22
+ # Install the lint/type toolchain into .venv without building _core.
23
+ _tools:
24
+ uv sync --no-install-project --group dev
25
+
26
+ # Lint and type-check Python.
27
+ check: _tools
28
+ uv run --no-sync ruff check .
29
+ uv run --no-sync ruff format --check .
30
+ uv run --no-sync basedpyright
31
+
32
+ # Format Python and C++ in place.
33
+ fmt: _tools
34
+ uv run --no-sync ruff format .
35
+ clang-format -i _core.cpp
fizzpy-0.3.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Xevion
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
fizzpy-0.3.0/PKG-INFO ADDED
@@ -0,0 +1,163 @@
1
+ Metadata-Version: 2.4
2
+ Name: fizzpy
3
+ Version: 0.3.0
4
+ Summary: A low-level TLS 1.3 client toolkit for Python, built on Facebook's Fizz
5
+ Author-Email: Xevion <xevion@xevion.dev>
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Operating System :: POSIX :: Linux
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: Internet :: WWW/HTTP
18
+ Classifier: Topic :: Security :: Cryptography
19
+ Classifier: Typing :: Typed
20
+ Project-URL: Repository, https://github.com/Xevion/fizz-py
21
+ Project-URL: Issues, https://github.com/Xevion/fizz-py/issues
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: certifi
24
+ Provides-Extra: test
25
+ Requires-Dist: pytest>=8; extra == "test"
26
+ Description-Content-Type: text/markdown
27
+
28
+ # fizzpy
29
+
30
+ A small TLS 1.3 HTTP client for Python, built on Facebook's
31
+ [Fizz](https://github.com/facebookincubator/fizz) (C++) via pybind11.
32
+
33
+ By default it offers a **post-quantum key exchange** — the standardized hybrid
34
+ `X25519MLKEM768` group (codepoint 4588). An ordinary `GET` negotiates a hybrid
35
+ ML-KEM handshake against servers that support it (Cloudflare, Google) and falls
36
+ back to classical X25519 against those that don't.
37
+
38
+ > TLS 1.3 only, HTTP/1.1 only. This is a focused toolkit / learning project,
39
+ > not a drop-in `requests` replacement. See [Status](#status).
40
+
41
+ ![A fizzpy ClientHello in Wireshark](docs/pq-handshake.png)
42
+
43
+ *A fizzpy request captured in Wireshark: the TLS 1.3 ClientHello offers the
44
+ post-quantum `X25519MLKEM768` group (`0x11ec`), and a custom marker extension
45
+ (`0xFE5A`, [see below](#custom-clienthello-extensions)) makes it unmistakably
46
+ ours — generated by [`examples/pq_marker.py`](examples/pq_marker.py).*
47
+
48
+ ## Install
49
+
50
+ ```sh
51
+ pip install fizzpy
52
+ ```
53
+
54
+ The Linux wheels are self-contained: folly, Fizz, and liboqs are statically
55
+ linked into the extension, so a fresh `pip install` has working post-quantum
56
+ TLS with no system packages to add. `certifi` (pulled in automatically) supplies
57
+ the default trust store.
58
+
59
+ Wheels are produced by CI (`manylinux_2_28`, x86_64, CPython 3.10–3.14); if one
60
+ isn't available for your platform yet, build from source — see
61
+ [BUILDING.md](BUILDING.md).
62
+
63
+ ## Usage
64
+
65
+ Synchronous:
66
+
67
+ ```python
68
+ import fizzpy
69
+
70
+ r = fizzpy.get("https://www.cloudflare.com")
71
+ print(r.status_code) # 200
72
+ print(r.headers["content-type"]) # text/html; charset=UTF-8
73
+ print(r.text[:64])
74
+
75
+ # The negotiated TLS 1.3 parameters are attached to every response:
76
+ print(r.tls)
77
+ # {'version': 'TLSv1.3', 'cipher': 'TLS_AES_128_GCM_SHA256',
78
+ # 'group': 'X25519MLKEM768', 'group_code': 4588,
79
+ # 'alpn': 'http/1.1', 'sni': 'www.cloudflare.com', 'peer_cert': '...'}
80
+ ```
81
+
82
+ Asynchronous (the same C++ core, resolved on the running event loop):
83
+
84
+ ```python
85
+ import asyncio
86
+ from fizzpy.aio import AsyncClient
87
+
88
+ async def main():
89
+ async with AsyncClient() as client:
90
+ r = await client.get("https://www.google.com")
91
+ print(r.status_code, r.tls["group"], r.tls["group_code"])
92
+
93
+ asyncio.run(main())
94
+ ```
95
+
96
+ ### Choosing the key exchange
97
+
98
+ The default offers `[x25519_mlkem768, x25519]`, mirroring how Chrome and Firefox
99
+ send both key shares. Override it per client:
100
+
101
+ ```python
102
+ import fizzpy
103
+ from fizzpy import NamedGroup
104
+
105
+ # Force post-quantum only (handshake fails if the server lacks ML-KEM):
106
+ pq = fizzpy.Client(groups=[NamedGroup.x25519_mlkem768])
107
+
108
+ # Classical only:
109
+ classical = fizzpy.Client(groups=[NamedGroup.x25519])
110
+ ```
111
+
112
+ ### Custom ClientHello extensions
113
+
114
+ fizzpy surfaces Fizz's low-level extension hook, so you can append arbitrary
115
+ extensions to the TLS 1.3 ClientHello. Each is an opaque `(type, data)` pair;
116
+ fizzpy rejects types it manages itself (key_share, supported_versions, …) and
117
+ out-of-range or duplicate types.
118
+
119
+ ```python
120
+ import fizzpy
121
+
122
+ client = fizzpy.Client(
123
+ extensions=[fizzpy.Extension(0xFE5A, b"hello from fizzpy")],
124
+ )
125
+ r = client.get("https://blog.cloudflare.com")
126
+ print(r.tls["group"]) # still negotiates X25519MLKEM768
127
+ ```
128
+
129
+ The extension is sent verbatim in the ClientHello — see it on the wire with
130
+ [`examples/pq_marker.py`](examples/pq_marker.py) and the screenshot above.
131
+
132
+ ### Certificate verification
133
+
134
+ Certificate chains are verified against the [certifi](https://github.com/certifi/python-certifi)
135
+ CA bundle and the hostname is checked against the certificate's SAN by default.
136
+ Point at a custom CA, or disable verification entirely:
137
+
138
+ ```python
139
+ fizzpy.Client(cafile="/path/to/ca.pem") # trust a specific CA
140
+ fizzpy.Client(verify=False) # accept any certificate (insecure)
141
+ ```
142
+
143
+ ## Status
144
+
145
+ What works today:
146
+
147
+ - TLS 1.3 handshake with classical or post-quantum (ML-KEM) key exchange
148
+ - `GET`/`POST`/`HEAD`/`PUT`/`DELETE`, sync and async
149
+ - HTTP/1.1 response framing (content-length, chunked, gzip/deflate)
150
+ - Connection keep-alive with a per-host pool; automatic redirect following
151
+ - Chain + hostname certificate verification, certifi default + custom CA trust
152
+
153
+ Current limitations:
154
+
155
+ - **TLS 1.3 only** (a Fizz constraint) — it cannot talk to TLS 1.2-only servers
156
+ - HTTP/1.1 only (no HTTP/2)
157
+
158
+ ## Building
159
+
160
+ The post-quantum handshake requires a Fizz built against
161
+ [liboqs](https://github.com/open-quantum-safe/liboqs). The CI wheels build the
162
+ whole folly + Fizz + liboqs tree from source inside `manylinux_2_28`; see
163
+ [BUILDING.md](BUILDING.md) for that recipe and for local development builds.