fizzpy 0.3.0__tar.gz → 0.5.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.
Files changed (75) hide show
  1. fizzpy-0.5.0/.github/manylinux-deps.Dockerfile +15 -0
  2. fizzpy-0.5.0/.github/workflows/bump-fizz.yml +84 -0
  3. {fizzpy-0.3.0 → fizzpy-0.5.0}/.github/workflows/check.yml +3 -3
  4. fizzpy-0.5.0/.github/workflows/wheels.yml +187 -0
  5. fizzpy-0.5.0/BUILDING.md +91 -0
  6. fizzpy-0.5.0/CHANGELOG.md +55 -0
  7. {fizzpy-0.3.0 → fizzpy-0.5.0}/CMakeLists.txt +15 -7
  8. {fizzpy-0.3.0 → fizzpy-0.5.0}/Justfile +23 -0
  9. fizzpy-0.5.0/PKG-INFO +109 -0
  10. fizzpy-0.5.0/README.md +74 -0
  11. {fizzpy-0.3.0 → fizzpy-0.5.0}/_core.cpp +434 -75
  12. fizzpy-0.5.0/docs/client.md +37 -0
  13. fizzpy-0.5.0/docs/limitations.md +16 -0
  14. fizzpy-0.5.0/docs/probe.md +89 -0
  15. fizzpy-0.5.0/docs/requests-httpx.md +94 -0
  16. fizzpy-0.5.0/docs/tls.md +113 -0
  17. fizzpy-0.5.0/examples/httpx_transport.py +47 -0
  18. fizzpy-0.5.0/examples/requests_adapter.py +45 -0
  19. {fizzpy-0.3.0 → fizzpy-0.5.0}/fizzpy/__init__.py +23 -7
  20. fizzpy-0.5.0/fizzpy/__main__.py +235 -0
  21. {fizzpy-0.3.0 → fizzpy-0.5.0}/fizzpy/_common.py +36 -4
  22. {fizzpy-0.3.0 → fizzpy-0.5.0}/fizzpy/_core.pyi +18 -0
  23. fizzpy-0.5.0/fizzpy/_tls.py +348 -0
  24. {fizzpy-0.3.0 → fizzpy-0.5.0}/fizzpy/aio.py +8 -3
  25. fizzpy-0.5.0/fizzpy/contrib/__init__.py +8 -0
  26. fizzpy-0.5.0/fizzpy/contrib/_context.py +153 -0
  27. fizzpy-0.5.0/fizzpy/contrib/httpx.py +89 -0
  28. fizzpy-0.5.0/fizzpy/contrib/requests.py +85 -0
  29. fizzpy-0.5.0/fizzpy/probe.py +512 -0
  30. {fizzpy-0.3.0 → fizzpy-0.5.0}/pyproject.toml +61 -15
  31. {fizzpy-0.3.0 → fizzpy-0.5.0}/scripts/build_fizz.py +5 -2
  32. {fizzpy-0.3.0 → fizzpy-0.5.0}/scripts/build_fizz_deps.py +5 -3
  33. fizzpy-0.5.0/scripts/build_fizz_deps_native.py +208 -0
  34. fizzpy-0.5.0/scripts/dev.py +226 -0
  35. fizzpy-0.5.0/scripts/fizz-tag.txt +1 -0
  36. fizzpy-0.5.0/scripts/repair_wheel_windows.py +47 -0
  37. fizzpy-0.5.0/scripts/wheel_smoke.py +42 -0
  38. fizzpy-0.5.0/tests/conftest.py +673 -0
  39. {fizzpy-0.3.0 → fizzpy-0.5.0}/tests/test_basic.py +6 -0
  40. fizzpy-0.5.0/tests/test_cli.py +153 -0
  41. fizzpy-0.5.0/tests/test_concurrency.py +56 -0
  42. fizzpy-0.5.0/tests/test_contrib_httpx.py +93 -0
  43. fizzpy-0.5.0/tests/test_contrib_requests.py +92 -0
  44. fizzpy-0.5.0/tests/test_fallback.py +55 -0
  45. fizzpy-0.5.0/tests/test_free_threading.py +54 -0
  46. fizzpy-0.5.0/tests/test_probe.py +318 -0
  47. fizzpy-0.5.0/tests/test_proxy.py +43 -0
  48. fizzpy-0.5.0/tests/test_sni.py +44 -0
  49. fizzpy-0.5.0/tests/test_timeout.py +105 -0
  50. {fizzpy-0.3.0 → fizzpy-0.5.0}/tests/test_verify.py +7 -3
  51. fizzpy-0.5.0/tests/test_wrap_socket.py +151 -0
  52. fizzpy-0.5.0/uv.lock +565 -0
  53. fizzpy-0.3.0/.github/manylinux-deps.Dockerfile +0 -16
  54. fizzpy-0.3.0/.github/workflows/wheels.yml +0 -118
  55. fizzpy-0.3.0/BUILDING.md +0 -57
  56. fizzpy-0.3.0/PKG-INFO +0 -163
  57. fizzpy-0.3.0/README.md +0 -136
  58. fizzpy-0.3.0/tests/conftest.py +0 -347
  59. fizzpy-0.3.0/uv.lock +0 -290
  60. {fizzpy-0.3.0 → fizzpy-0.5.0}/.github/workflows/publish.yml +0 -0
  61. {fizzpy-0.3.0 → fizzpy-0.5.0}/.gitignore +0 -0
  62. {fizzpy-0.3.0 → fizzpy-0.5.0}/.vscode/c_cpp_properties.json +0 -0
  63. {fizzpy-0.3.0 → fizzpy-0.5.0}/.vscode/settings.json +0 -0
  64. {fizzpy-0.3.0 → fizzpy-0.5.0}/LICENSE +0 -0
  65. {fizzpy-0.3.0 → fizzpy-0.5.0}/docs/pq-handshake.png +0 -0
  66. {fizzpy-0.3.0 → fizzpy-0.5.0}/examples/pq_marker.py +0 -0
  67. {fizzpy-0.3.0 → fizzpy-0.5.0}/fizzpy/_http.py +0 -0
  68. {fizzpy-0.3.0 → fizzpy-0.5.0}/fizzpy/_transport.py +0 -0
  69. {fizzpy-0.3.0 → fizzpy-0.5.0}/pytest.ini +0 -0
  70. {fizzpy-0.3.0 → fizzpy-0.5.0}/tests/_clienthello.py +0 -0
  71. {fizzpy-0.3.0 → fizzpy-0.5.0}/tests/test_extensions.py +0 -0
  72. {fizzpy-0.3.0 → fizzpy-0.5.0}/tests/test_http.py +0 -0
  73. {fizzpy-0.3.0 → fizzpy-0.5.0}/tests/test_integration.py +0 -0
  74. {fizzpy-0.3.0 → fizzpy-0.5.0}/tests/test_keepalive.py +0 -0
  75. {fizzpy-0.3.0 → fizzpy-0.5.0}/tests/test_redirects.py +0 -0
@@ -0,0 +1,15 @@
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 the deps_image job in .github/workflows/wheels.yml when build_fizz_deps.py or
4
+ # fizz-tag.txt changes. The dep prefixes land in $FIZZPY_BUILD_INFO/prefix.txt,
5
+ # which CMake 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
+ ENV GETDEPS_SCRATCH=/opt/fizz-deps \
10
+ FIZZPY_BUILD_INFO=/opt/fizzpy-build \
11
+ FIZZ_SRC=/opt/fizz-src
12
+
13
+ # The script reads the pin from fizz-tag.txt beside itself.
14
+ COPY scripts/build_fizz_deps.py scripts/fizz-tag.txt /opt/
15
+ RUN python3 /opt/build_fizz_deps.py
@@ -0,0 +1,84 @@
1
+ name: Bump Fizz
2
+
3
+ # Fizz tags weekly and the pin is the only thing keeping wheels current, so a
4
+ # monthly job opens a PR whenever a newer release exists. Pushes and PRs made with
5
+ # GITHUB_TOKEN don't trigger other workflows, but workflow_dispatch is exempt: the
6
+ # job dispatches the full wheel matrix and the lint check on the bump branch, and
7
+ # their results attach to the PR through the branch's head commit.
8
+ on:
9
+ schedule:
10
+ - cron: "0 6 1 * *"
11
+ workflow_dispatch:
12
+
13
+ permissions:
14
+ contents: write
15
+ pull-requests: write
16
+ actions: write
17
+
18
+ jobs:
19
+ bump:
20
+ name: Bump Fizz pin
21
+ runs-on: ubuntu-latest
22
+ timeout-minutes: 10
23
+ env:
24
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
25
+ BASE: ${{ github.event.repository.default_branch }}
26
+ steps:
27
+ - uses: actions/checkout@v7
28
+
29
+ - name: Find the newest Fizz release
30
+ id: latest
31
+ run: |
32
+ current=$(tr -d '[:space:]' < scripts/fizz-tag.txt)
33
+ latest=$(git ls-remote --tags --refs https://github.com/facebookincubator/fizz \
34
+ | sed 's|.*refs/tags/||' \
35
+ | grep -E '^v[0-9]{4}\.[0-9]{2}\.[0-9]{2}\.[0-9]{2}$' \
36
+ | sort -V | tail -n1)
37
+ if [ -z "$latest" ]; then
38
+ echo "no release tags found" >&2
39
+ exit 1
40
+ fi
41
+ newest=$(printf '%s\n%s\n' "$current" "$latest" | sort -V | tail -n1)
42
+ if [ "$latest" = "$current" ] || [ "$newest" != "$latest" ]; then
43
+ echo "pin $current is already the newest release"
44
+ echo "bump=false" >> "$GITHUB_OUTPUT"
45
+ exit 0
46
+ fi
47
+ if git ls-remote --exit-code --heads origin "bump/fizz-$latest" > /dev/null; then
48
+ echo "bump/fizz-$latest already exists; leaving it alone"
49
+ echo "bump=false" >> "$GITHUB_OUTPUT"
50
+ exit 0
51
+ fi
52
+ {
53
+ echo "bump=true"
54
+ echo "current=$current"
55
+ echo "latest=$latest"
56
+ } >> "$GITHUB_OUTPUT"
57
+
58
+ - name: Open the bump PR
59
+ if: steps.latest.outputs.bump == 'true'
60
+ env:
61
+ CURRENT: ${{ steps.latest.outputs.current }}
62
+ LATEST: ${{ steps.latest.outputs.latest }}
63
+ run: |
64
+ branch="bump/fizz-$LATEST"
65
+ git config user.name "github-actions[bot]"
66
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
67
+ git switch -c "$branch"
68
+ echo "$LATEST" > scripts/fizz-tag.txt
69
+ git commit -am "build: bump Fizz to $LATEST"
70
+ git push origin "$branch"
71
+
72
+ gh pr create --base "$BASE" --head "$branch" \
73
+ --title "build: bump Fizz to $LATEST" \
74
+ --body "Bumps the Fizz pin from $CURRENT to $LATEST ([compare](https://github.com/facebookincubator/fizz/compare/$CURRENT...$LATEST)). The wheel matrix and lint check were dispatched on this branch; merge once they are green."
75
+
76
+ gh workflow run wheels.yml --ref "$branch"
77
+ gh workflow run check.yml --ref "$branch"
78
+
79
+ # Older bump PRs are superseded by this one.
80
+ gh pr list --state open --json number,headRefName \
81
+ --jq ".[] | select(.headRefName | startswith(\"bump/fizz-\")) | select(.headRefName != \"$branch\") | .number" \
82
+ | while read -r number; do
83
+ gh pr close "$number" --delete-branch --comment "Superseded by $LATEST."
84
+ done
@@ -9,7 +9,7 @@ on:
9
9
 
10
10
  concurrency:
11
11
  group: ${{ github.workflow }}-${{ github.ref }}
12
- cancel-in-progress: true
12
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
13
13
 
14
14
  jobs:
15
15
  quality:
@@ -17,10 +17,10 @@ jobs:
17
17
  runs-on: ubuntu-latest
18
18
  timeout-minutes: 10
19
19
  steps:
20
- - uses: actions/checkout@v6
20
+ - uses: actions/checkout@v7
21
21
 
22
22
  - name: Install uv
23
- uses: astral-sh/setup-uv@v8.2.0
23
+ uses: astral-sh/setup-uv@v10.3.3
24
24
  with:
25
25
  enable-cache: true
26
26
 
@@ -0,0 +1,187 @@
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: false
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@v7
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', 'scripts/fizz-tag.txt', '.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@v7
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, cp315, cp314t, cp315t]
96
+ steps:
97
+ - uses: actions/checkout@v7
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.3.5
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
119
+
120
+ build_wheels_native:
121
+ # macOS and Windows have no prebuilt deps image, so each runner builds the
122
+ # folly+fizz+liboqs tree from source (cached). Skipped on PRs — PR feedback
123
+ # stays on the fast Linux path; these run on master, tags, and dispatch.
124
+ name: Wheel ${{ matrix.os }}
125
+ if: github.event_name != 'pull_request'
126
+ runs-on: ${{ matrix.os }}
127
+ timeout-minutes: 120
128
+ strategy:
129
+ fail-fast: false
130
+ matrix:
131
+ # macos-15-intel = x86_64, macos-15 = arm64 (native per runner). windows-2022
132
+ # ships VS 2022 (vc143); windows-latest moved to VS 2026, whose toolset Boost
133
+ # 1.83's bootstrap doesn't recognize. Interpreters (cp310-cp314) come from
134
+ # [tool.cibuildwheel] build in pyproject.toml.
135
+ os: [macos-15-intel, macos-15, windows-2022]
136
+ steps:
137
+ - uses: actions/checkout@v7
138
+
139
+ # before-all runs on the host on macOS/Windows (no container), so these are
140
+ # inherited by the deps build and the _core configure without CIBW_ENVIRONMENT.
141
+ - name: Resolve deps paths (unix)
142
+ if: runner.os != 'Windows'
143
+ shell: bash
144
+ run: |
145
+ {
146
+ echo "GETDEPS_SCRATCH=$HOME/fizz-deps"
147
+ echo "FIZZPY_BUILD_INFO=$HOME/fizzpy-build"
148
+ echo "FIZZ_SRC=$HOME/fizz-src"
149
+ # Match the runner's macOS major: getdeps pulls Homebrew libs (openssl,
150
+ # sodium, …) built for this OS, so the wheel can't claim an older target
151
+ # than they carry. Also satisfies folly's C++17 (>= 10.13) requirement.
152
+ echo "MACOSX_DEPLOYMENT_TARGET=$(sw_vers -productVersion | cut -d. -f1).0"
153
+ } >> "$GITHUB_ENV"
154
+
155
+ - name: Resolve deps paths and locate MSVC (windows)
156
+ if: runner.os == 'Windows'
157
+ shell: bash
158
+ run: |
159
+ vswhere="/c/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe"
160
+ install=$("$vswhere" -latest -property installationPath)
161
+ {
162
+ echo "GETDEPS_SCRATCH=C:/fizz-deps"
163
+ echo "FIZZPY_BUILD_INFO=C:/fizzpy-build"
164
+ echo "FIZZ_SRC=C:/fizz-src"
165
+ # getdeps only globs for VS 2022; point it at whatever VS is installed.
166
+ echo "VCVARS_PATH=$install\\VC\\Auxiliary\\Build\\vcvarsall.bat"
167
+ } >> "$GITHUB_ENV"
168
+
169
+ # The getdeps tree is the ~40-min cost; key it on the build script + the Fizz
170
+ # pin (scripts/fizz-tag.txt) so a warm cache turns before-all into a near no-op.
171
+ - name: Cache deps tree
172
+ uses: actions/cache@v6
173
+ with:
174
+ path: |
175
+ ${{ env.GETDEPS_SCRATCH }}
176
+ ${{ env.FIZZPY_BUILD_INFO }}
177
+ ${{ env.FIZZ_SRC }}
178
+ key: deps-${{ matrix.os }}-${{ env.MACOSX_DEPLOYMENT_TARGET }}-${{ hashFiles('scripts/fizz-tag.txt', 'scripts/build_fizz_deps_native.py') }}
179
+
180
+ - name: Build wheels
181
+ uses: pypa/cibuildwheel@v4.3.5
182
+
183
+ - uses: actions/upload-artifact@v7
184
+ with:
185
+ name: wheels-${{ matrix.os }}
186
+ path: wheelhouse/*.whl
187
+ if-no-files-found: error
@@ -0,0 +1,91 @@
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 three build paths, for three different purposes:
9
+
10
+ | | Container dev (`just dev-build`) | Homebrew dev (`just build`) | CI wheels (`scripts/build_fizz_deps.py`) |
11
+ | --- | --- | --- | --- |
12
+ | Fizz/liboqs source | getdeps tree in `.dev/getdeps` | Homebrew + from-source Fizz | getdeps, all from source |
13
+ | Linkage | static, against the CI tree | static Fizz, shared liboqs via rpath | fully static-PIC, self-contained `.so` |
14
+ | Where | `manylinux_2_28` container | your machine | `manylinux_2_28` container |
15
+ | Optimized for | "builds here == builds in CI" | fastest iteration | deterministic, portable distribution |
16
+
17
+ ## Container development (`just dev-*`) — recommended
18
+
19
+ The Homebrew loop links `_core` against Homebrew's *shared* folly, so a Homebrew
20
+ folly bump (now compiled for a newer glibc than many hosts) silently breaks every
21
+ local build. The container loop sidesteps that: it builds and runs `_core` inside
22
+ the same `manylinux_2_28` image the wheels use, against the prebuilt static
23
+ folly + Fizz + liboqs tree in `.dev/getdeps`. "Builds locally" and "builds in CI"
24
+ become the same statement.
25
+
26
+ ```sh
27
+ just dev-up # create/start the container + one-time system + pip setup
28
+ just dev-build # editable-build _core against the getdeps tree (~10s inner loop)
29
+ just dev-test # offline suite; `just dev-test "-- -m network"` runs live-network tests
30
+ just dev-shell # interactive bash inside the container
31
+ just dev-down # stop + remove the container
32
+ ```
33
+
34
+ The trick that needs no path rewriting: the getdeps tree bakes absolute
35
+ `/project/...` paths into its CMake configs (it was built at `/project`), so the
36
+ repo is bind-mounted at `/project` inside the container and every baked path
37
+ resolves. A persistent container plus `docker exec` keeps the inner loop fast
38
+ (no per-change `docker run`/image build).
39
+
40
+ This requires the `.dev/getdeps` tree on disk (gitignored). Rebuild it inside the
41
+ image with `scripts/build_fizz_deps.py` if it's missing (~40 min, one-time). The
42
+ orchestration lives in `scripts/dev.py`.
43
+
44
+ ## Homebrew development (`just build`) — fastest, host-dependent
45
+
46
+ Fast iteration when your host glibc is new enough for the current Homebrew folly
47
+ bottle. If `import fizzpy` fails with a `GLIBC_2.xx not found` error, your host is
48
+ too old — use the container loop above.
49
+
50
+ ```sh
51
+ brew install fizz liboqs # fizz pulls folly, boost, glog, gflags, libsodium, double-conversion
52
+ pip install -e .[dev] --no-build-isolation # or: pip install scikit-build-core pybind11 pybind11-stubgen ruff pytest
53
+
54
+ just build # build OQS Fizz if needed (cached), then editable-install _core
55
+ just test # offline test suite; `just test ""` includes live network tests
56
+ just check # ruff lint
57
+ just fmt # ruff format + clang-format _core.cpp
58
+ ```
59
+
60
+ `just build` runs `scripts/build_fizz.py ensure`, which compiles a liboqs-enabled
61
+ Fizz into the gitignored `.dev/fizz-prefix` once (subsequent runs are a no-op;
62
+ `python scripts/build_fizz.py ensure --force` rebuilds), then editable-installs
63
+ `_core` with `find_package(fizz)` pointed at that prefix.
64
+
65
+ - **C++20 is required** — current Folly (2026.06) dropped C++17.
66
+ - Fizz and Folly are versioned together. The Fizz tag lives in
67
+ `scripts/fizz-tag.txt`, shared by every build path; it must match the Homebrew
68
+ Folly bottle here, or the ABI won't match (`FIZZ_TAG=... just build` overrides
69
+ it when brew lags). A monthly workflow (`bump-fizz.yml`) opens a PR when a newer
70
+ Fizz release exists.
71
+ - `find_package(liboqs CONFIG)` inside Fizz auto-sets `FIZZ_HAVE_OQS` and links
72
+ `OQS::oqs`. `BUILD_EXAMPLES=OFF` is required: the bundled FizzTool fails to
73
+ compile under OpenSSL 3.6 (`-Werror` on the deprecated `EVP_PKEY_cmp`); the
74
+ library itself is fine.
75
+
76
+ ## Portable wheels (CI)
77
+
78
+ `scripts/build_fizz_deps.py` is cibuildwheel's `before-all`. It builds the whole
79
+ folly + Fizz + liboqs tree once per `manylinux_2_28` container as static-PIC
80
+ archives, which `_core` links in wholesale — a single self-contained extension
81
+ with exactly one copy of folly. (Using getdeps `--shared-lib` instead would bake
82
+ folly into `libfizz.so` *and* drag it into `_core` again, double-registering
83
+ folly's gflags globals and aborting at import.) The build settings live in
84
+ `[tool.cibuildwheel]` in `pyproject.toml`.
85
+
86
+ ## Verify
87
+
88
+ ```sh
89
+ python -c "import fizzpy; print(fizzpy.get('https://www.cloudflare.com').tls)"
90
+ # group should be 'X25519MLKEM768', group_code 4588
91
+ ```
@@ -0,0 +1,55 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.5.0] - 2026-11-15
9
+
10
+ ### Added
11
+
12
+ - `fizzpy.wrap_socket(sock, server_hostname, config)` performs a Fizz TLS 1.3 handshake over an already-connected socket and returns a blocking, `ssl.SSLSocket`-shaped `TlsSocket`. `TlsConfig` holds the handshake options (verification, CA file, key groups, ALPN, extensions, timeout). An empty server name sends no SNI extension.
13
+ - `fizzpy.contrib.requests.FizzAdapter` mounts Fizz under `requests` and urllib3. Install it with `pip install fizzpy[requests]`.
14
+ - `fizzpy.contrib.httpx.FizzHTTPTransport` does the same for synchronous `httpx`. Install it with `pip install fizzpy[httpx]`. It rejects `verify=`, `cert=` and `http2=True` instead of ignoring them.
15
+ - Opt-in TLS 1.2 fallback: `FizzAdapter(fallback=True)` and `FizzHTTPTransport(fallback=True)` retry over the standard library `ssl` module when the server rejects TLS 1.3 with a `protocol_version` alert. The retry is a classical, non post-quantum handshake. Certificate and hostname failures are never retried.
16
+ - Plain `CONNECT` proxies work with the adapters. The HTTP client opens the tunnel and fizzpy handshakes over it. Proxies that themselves require TLS are not supported.
17
+ - `fizzpy probe`, a command line check for which hosts complete a post-quantum handshake. It takes hosts or a file, can print JSON, and `--require-pq` exits 1 unless every target is post-quantum. `fizzpy.probe.probe` and `probe_many` are the Python API. See `docs/probe.md`.
18
+ - `fizzpy.supports_post_quantum()` reports whether the installed build can do the hybrid key exchange. It is always true for the wheels. A source build against a Fizz without liboqs returns false and now defaults to classical x25519 only, where it previously could not generate the hybrid key share.
19
+ - TLS handshake errors carry a `fizz_alert` attribute with the alert name (for example `protocol_version`) and a `fizz_alert_remote` attribute saying whether the peer sent it.
20
+ - Wheels for CPython 3.15 and for free-threaded CPython 3.14t and 3.15t, on every platform that has wheels.
21
+
22
+ ### Changed
23
+
24
+ - Callers must update exception handling. Handshake failures from `Client`, `AsyncClient` and the new APIs now raise `ssl.SSLError`, or `ssl.SSLCertVerificationError` for certificate and hostname failures, instead of `ConnectionError`. Read and write timeouts raise `socket.timeout` instead of `ConnectionError`. Both are `OSError` subclasses, so `except OSError` still catches them, but `except ConnectionError` no longer does. Connection setup failures such as a refused connection still raise `ConnectionError`.
25
+ - The `timeout` option now bounds each read and write as well as the handshake, so a stalled peer raises `socket.timeout` instead of hanging. `timeout=None` means no deadline, and a zero or negative timeout fails fast rather than blocking forever.
26
+ - The wheels now bundle Fizz v2026.10.26.00 (previously v2026.06.08.00).
27
+
28
+ ### Fixed
29
+
30
+ - The Windows wheels no longer risk crashing the process at interpreter exit. The shared event loop thread is now left running instead of being joined while the extension unloads.
31
+
32
+ ## [0.4.0] - 2026-06-16
33
+
34
+ ### Added
35
+
36
+ - Wheels for macOS (arm64 and x86_64) and Windows (x64), alongside the existing Linux x86_64 wheels. Like the Linux wheels, they are self-contained and include post-quantum support.
37
+
38
+ ## [0.3.0] - 2026-06-13
39
+
40
+ Initial release, supporting TLS 1.3 and HTTP/1.1 only.
41
+
42
+ ### Added
43
+
44
+ - TLS 1.3 HTTP client built on Facebook's Fizz: synchronous `fizzpy.Client` with module-level `get`, `post`, `head`, `put`, `delete` and `request`, and an asyncio `fizzpy.aio.AsyncClient` over the same native core.
45
+ - Post-quantum key exchange by default: the client offers the hybrid `X25519MLKEM768` group and classical `x25519`, and falls back to x25519 against servers without ML-KEM. Use `groups=[NamedGroup.x25519_mlkem768]` to require post-quantum or `groups=[NamedGroup.x25519]` to disable it.
46
+ - Every response carries the negotiated TLS parameters in `Response.tls` (version, cipher, group, ALPN, SNI, peer certificate).
47
+ - Certificate chain and hostname verification against the `certifi` bundle by default, with `cafile=` for a custom CA and `verify=False` to turn it off.
48
+ - HTTP/1.1 responses with content-length, chunked and gzip/deflate bodies, per-host keep-alive connection pooling, and automatic redirect following with `follow_redirects` and `max_redirects` (`TooManyRedirects` when exceeded).
49
+ - `Extension(type, data)` to append custom extensions to the ClientHello, through `extensions=`.
50
+ - Type stubs for the native `fizzpy._core` module.
51
+ - Self-contained wheels for Linux x86_64 (`manylinux_2_28`), CPython 3.10 to 3.14. Fizz, folly and liboqs are linked in, so no system packages are needed.
52
+
53
+ [0.5.0]: https://github.com/Xevion/fizz-py/compare/v0.4.0...v0.5.0
54
+ [0.4.0]: https://github.com/Xevion/fizz-py/compare/v0.3.0...v0.4.0
55
+ [0.3.0]: https://github.com/Xevion/fizz-py/releases/tag/v0.3.0
@@ -7,15 +7,15 @@ project(
7
7
  VERSION ${SKBUILD_PROJECT_VERSION}
8
8
  LANGUAGES CXX)
9
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")
10
+ # Wheels target Linux, macOS, and Windows. Other platforms stop here before the
11
+ # (~40 min) folly + fizz + liboqs source build an sdist install would attempt.
12
+ if(NOT CMAKE_SYSTEM_NAME MATCHES "^(Linux|Darwin|Windows)$")
14
13
  message(
15
14
  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).")
15
+ "fizzpy publishes prebuilt wheels for Linux, macOS, and Windows; "
16
+ "${CMAKE_SYSTEM_NAME} is not supported. Building from source needs the Fizz "
17
+ "toolchain — see BUILDING.md "
18
+ "(https://github.com/Xevion/fizz-py/blob/master/BUILDING.md).")
19
19
  endif()
20
20
 
21
21
  # Fizz's CMake config pulls in its bundled FindSodium.cmake. Homebrew installs
@@ -85,4 +85,12 @@ if(TARGET Folly::folly_io_async_scoped_event_base_thread)
85
85
  target_link_libraries(_core PRIVATE Folly::folly_io_async_scoped_event_base_thread)
86
86
  endif()
87
87
 
88
+ # Static deps need Windows system libs their getdeps configs don't propagate as
89
+ # interface links: bcrypt (RNG: libevent, liboqs), crypt32 (OpenSSL cert store),
90
+ # ws2_32 (sockets), iphlpapi (folly GetAdaptersAddresses), advapi32 (OpenSSL/
91
+ # libevent registry + RNG fallback). Link them on the consumer.
92
+ if(WIN32)
93
+ target_link_libraries(_core PRIVATE bcrypt crypt32 ws2_32 iphlpapi advapi32)
94
+ endif()
95
+
88
96
  install(TARGETS _core LIBRARY DESTINATION fizzpy)
@@ -19,6 +19,29 @@ wheel:
19
19
  test *args="-m 'not network'":
20
20
  pytest {{args}}
21
21
 
22
+ # Containerized dev loop (recommended): build + test inside the manylinux image
23
+ # the wheels use, against the prebuilt .dev/getdeps tree. Isolated from host
24
+ # Homebrew/glibc drift. See scripts/dev.py.
25
+ dev-up:
26
+ python scripts/dev.py up
27
+
28
+ # Editable-build _core in the container (the ~10s inner loop).
29
+ dev-build:
30
+ python scripts/dev.py build
31
+
32
+ # Run the suite in the container. `just dev-test "-- -m network"` runs the
33
+ # live-network tests; pass any other pytest args after a `--` the same way.
34
+ dev-test *args:
35
+ python scripts/dev.py test {{args}}
36
+
37
+ # Interactive shell inside the dev container.
38
+ dev-shell:
39
+ python scripts/dev.py shell
40
+
41
+ # Stop + remove the dev container.
42
+ dev-down:
43
+ python scripts/dev.py down
44
+
22
45
  # Install the lint/type toolchain into .venv without building _core.
23
46
  _tools:
24
47
  uv sync --no-install-project --group dev
fizzpy-0.5.0/PKG-INFO ADDED
@@ -0,0 +1,109 @@
1
+ Metadata-Version: 2.4
2
+ Name: fizzpy
3
+ Version: 0.5.0
4
+ Summary: Post-quantum TLS 1.3 for Python — plug it under requests/urllib3 or use the built-in client. 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: Operating System :: MacOS
12
+ Classifier: Operating System :: Microsoft :: Windows
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Programming Language :: Python :: 3.15
20
+ Classifier: Programming Language :: Python :: Free Threading :: 2 - Beta
21
+ Classifier: Topic :: Internet :: WWW/HTTP
22
+ Classifier: Topic :: Security :: Cryptography
23
+ Classifier: Typing :: Typed
24
+ Project-URL: Repository, https://github.com/Xevion/fizz-py
25
+ Project-URL: Issues, https://github.com/Xevion/fizz-py/issues
26
+ Requires-Python: >=3.10
27
+ Requires-Dist: certifi
28
+ Provides-Extra: test
29
+ Requires-Dist: pytest>=8; extra == "test"
30
+ Provides-Extra: requests
31
+ Requires-Dist: requests; extra == "requests"
32
+ Provides-Extra: httpx
33
+ Requires-Dist: httpx; extra == "httpx"
34
+ Description-Content-Type: text/markdown
35
+
36
+ # fizzpy
37
+
38
+ Post-quantum TLS 1.3 for Python. Keep the HTTP client you already use and swap only
39
+ the TLS: fizzpy performs the handshake with the hybrid `X25519MLKEM768` key exchange,
40
+ built on Facebook's [Fizz](https://github.com/facebookincubator/fizz) (C++).
41
+
42
+ **Why.** The key exchange is the part of TLS exposed to "harvest now, decrypt later"
43
+ recording, which is why browsers moved to hybrid ML-KEM. In Python, ML-KEM arrived with
44
+ OpenSSL 3.5, so an interpreter linked against anything older cannot negotiate it.
45
+ fizzpy carries its own TLS stack (Fizz and liboqs), so it works there, and it can
46
+ check whether the servers you run or depend on support post-quantum key exchange.
47
+ Beta: the API may change before 1.0.
48
+
49
+ ```python
50
+ import requests
51
+ from fizzpy.contrib.requests import FizzAdapter
52
+
53
+ session = requests.Session()
54
+ session.mount("https://", FizzAdapter())
55
+ print(session.get("https://www.cloudflare.com").status_code)
56
+ ```
57
+
58
+ requests still owns cookies, redirects, retries and pooling; fizzpy only does the
59
+ handshake. The same works under [httpx](docs/requests-httpx.md#httpx), on any
60
+ [connected socket](docs/tls.md#any-socket-wrap_socket), or through the small
61
+ [built-in client](docs/client.md).
62
+
63
+ ![A fizzpy ClientHello in Wireshark](https://raw.githubusercontent.com/Xevion/fizz-py/master/docs/pq-handshake.png)
64
+
65
+ *A fizzpy ClientHello in Wireshark: it offers `X25519MLKEM768` (`0x11ec`) plus a
66
+ custom marker extension from [`examples/pq_marker.py`](examples/pq_marker.py).*
67
+
68
+ ## Check a server
69
+
70
+ ```console
71
+ $ fizzpy probe www.cloudflare.com github.com
72
+ HOST VERDICT PREFERRED ML-KEM x25519 P-256 P-384
73
+ www.cloudflare.com post-quantum X25519MLKEM768 ok ok ok ok
74
+ github.com classical-only x25519 rej ok ok rej
75
+ ```
76
+
77
+ One handshake per key-exchange group tells "supports ML-KEM" apart from "prefers
78
+ x25519", and finds hosts where the larger post-quantum hello gets a connection reset.
79
+ It scales to thousands of hosts and prints JSON. See [the probe guide](docs/probe.md).
80
+
81
+ ## Install
82
+
83
+ ```sh
84
+ pip install fizzpy
85
+ ```
86
+
87
+ Wheels are self-contained (Fizz and liboqs are built in) for Linux x86_64, macOS and
88
+ Windows x64, CPython 3.10 to 3.15 including free-threaded builds. For anything else,
89
+ see [BUILDING.md](BUILDING.md).
90
+
91
+ ## Limits
92
+
93
+ - TLS 1.3 only, so TLS 1.2-only servers fail unless you opt into
94
+ [a classical fallback](docs/tls.md#reaching-tls-12-only-hosts).
95
+ - HTTP/1.1 only.
96
+ - No client certificates yet.
97
+
98
+ More in [docs/limitations.md](docs/limitations.md).
99
+
100
+ ## Docs
101
+
102
+ - [Under requests and httpx](docs/requests-httpx.md)
103
+ - [Handshake control](docs/tls.md): `wrap_socket`, `TlsConfig`, key exchange,
104
+ extensions, certificates
105
+ - [The built-in client](docs/client.md)
106
+ - [The probe](docs/probe.md)
107
+ - [Building](BUILDING.md)
108
+
109
+ MIT licensed.