Commit 656ae05
Add web-transport-ffi: UniFFI bindings for Swift, Kotlin, Python (#239)
* web-transport-ffi: scaffold UniFFI crate
Initial cdylib+staticlib crate with proc-macro UniFFI 0.29, a hello()
export, and the uniffi-bindgen binary. Verified scaffolding generates
Python, Swift, and Kotlin bindings from the library metadata.
https://claude.ai/code/session_01Q2bHvKtyjMP6Coh7pVrqdU
* web-transport-ffi: add build.sh, pyproject.toml, and async smoke export
build.sh produces per-language bindings tarballs in --bindings-only mode
and cross-compiled native lib tarballs otherwise (universal-darwin lipo,
cargo-ndk for Android, staticlib-only for iOS). pyproject.toml wires
maturin with the uniffi binding generator. Added a hello_async() export
to validate the tokio async runtime integration end-to-end through a
locally built wheel.
https://claude.ai/code/session_01Q2bHvKtyjMP6Coh7pVrqdU
* web-transport-ffi: port to UniFFI 0.31 + add Kotlin/Swift skeletons
Replaces the PyO3 surface in rs/web-transport-ffi with a UniFFI 0.31
crate that targets Python, Kotlin, and Swift from one codebase. Mirrors
the moq-ffi consolidation pattern:
- Single LazyLock<Handle> tokio runtime in src/ffi.rs; every async
export awaits a JoinHandle from RUNTIME.spawn (no async_runtime =
"tokio" — uniffi just polls JoinHandles).
- Flat WebTransportError enum with the same mapping helpers as
web-transport-python's errors.rs; the foreign-language wrappers will
reconstruct the legacy exception hierarchy on top.
- Cancellable per-stream Mutex<Stream> + CancellationToken pattern
preserved from web-transport-python so reset()/stop() interrupts
in-flight write/read and sends RESET_STREAM/STOP_SENDING.
- Drop on Session/Client/Server enters RUNTIME before touching quinn so
the connection-close timer registers (moq ae506266).
- crate-type = ["staticlib", "cdylib"] only (no "lib", per moq cfc35faf)
with src/test.rs as a cfg-gated module rather than tests/echo.rs.
- aws-lc-rs is the default; ring is available behind --features ring.
Also lands:
- kt/ Kotlin Multiplatform skeleton (dev.moq:web-transport artifactId,
JVM + Android variants, JNA + jniLibs resource layouts). package.sh
uses bash 3.2-compatible case statements instead of declare -A.
- swift/ Swift Package Manager skeleton (WebTransport / WebTransportFFI,
Package.swift binaryTarget points at the GH Release XCFramework). No
publish.sh — the SPM mirror is deferred per the rollout plan.
- web-transport-quinn added to [workspace.dependencies] so the FFI
crate can pick up the right TLS provider via feature gates.
cargo test -p web-transport-ffi passes the round-trip echo + 5 unit
tests under both --features aws-lc-rs and --features ring.
* web-transport: complete UniFFI port — Python wrapper, workflows, cleanup
Replaces the legacy PyO3 binding (rs/web-transport-python) with a
maturin+UniFFI wheel built on top of rs/web-transport-ffi, adds the
GitHub release pipeline that fans out from web-transport-ffi-v* tags
to Python/Kotlin/Swift downstream workflows, and removes the now-dead
web-transport-python crate.
Python wrapper (py/web-transport):
- pyproject.toml: name → web-transport-rs (bare webtransport is squatted
on PyPI; PEP 503 normalization collapses web-transport to the same
name). module-name → web_transport._uniffi, manifest-path points at
rs/web-transport-ffi. Dynamic version, Python ≥ 3.10.
- python/web_transport/_errors.py: pure-Python legacy exception class
hierarchy + reraise() translator. Maps each _uniffi.WebTransportError
variant subclass back to the legacy SessionClosedByPeer /
StreamClosedLocally / etc. classes with the same .source / .code /
.reason / .kind / .partial / .expected / .limit attributes existing
user code inspects.
- python/web_transport/__init__.py: full rewrite as a thin wrapper
around _uniffi. Every async/sync method wraps the call in
try/except _uniffi.WebTransportError → reraise(e), preserving the
pre-UniFFI public API (Client, Server, Session, SessionRequest,
SendStream, RecvStream + async context-managers + async iterators).
- _web_transport.pyi deleted (uniffi generates its own typed module).
Rust:
- error.rs: dropped #[uniffi(flat_error)] so the foreign-side variants
expose structured fields (SessionClosedByPeer.code,
StreamIncompleteRead.partial, etc.) — flat_error stripped them to
just the Display string. Renamed SessionClosedByPeer.source field
to .closed_by (thiserror reserves source for its own use).
- Cargo.toml workspace: drop rs/web-transport-python member.
- justfile: feature-powerset excludes web-transport-ffi (aws-lc-rs and
ring are mutually exclusive at link time) and explicitly checks both
TLS providers. Adds build-ffi recipes.
- release-plz.toml: collapsed to a comment since web-transport-ffi now
follows release-plz defaults (publishes to crates.io, auto-pushes the
tag that drives the downstream workflows).
Build script (rs/web-transport-ffi/build.sh):
- Rewritten in moq-shape: staging-dir output, --archive opt-in, plain
Linux drops the .a copy (moq de7cc60d), uniffi-bindgen invoked as
`cargo run --package web-transport-ffi --bin uniffi-bindgen` to avoid
workspace bin ambiguity (moq d9878e7b). Verified end-to-end with
`./build.sh --bindings-only` producing kotlin/swift/python bindings.
GitHub workflows (.github/):
- scripts/release.sh: shared parse-version / prev-tag / create helper
(copied verbatim from moq).
- workflows/release-rs.yml: rename from release.yml, switches to the
create-github-app-token + release-plz pattern so the auto-pushed
web-transport-ffi-v* tag fires the downstream workflows. Falls back
to GITHUB_TOKEN when the App secrets aren't configured.
- workflows/release-py.yml: full rewrite, tag-triggered
(web-transport-ffi-v*), 5-target manylinux_2_28 matrix +
before-script-linux unset TARGET_CC/CXX on aarch64 (moq 0d59730d) +
sdist job + verify-version job that asserts the tag matches
rs/web-transport-ffi/Cargo.toml. Publish gated on PUBLISH_PYTHON.
- workflows/release-kt.yml: new. 7-target build matrix (Android +
desktop JVM), bindings job, package job that calls
kt/scripts/package.sh and publishes to Maven Central via
com.vanniktech.maven.publish (publish gated on PUBLISH_KOTLIN).
- workflows/release-swift.yml: new. iOS + macOS matrix, package job
assembles the XCFramework, release-assets job uploads the
XCFramework.zip + Swift package tarball to the GitHub Release via
release.sh create. Placeholder publish job behind PUBLISH_SPM for
the future SPM mirror.
Workspace cleanup:
- rs/web-transport-python/ deleted in its entirety.
- README.md + CLAUDE.md updated to document web-transport-ffi and the
three language-binding outputs.
Verification:
- cargo check --workspace passes.
- cargo clippy -p web-transport-ffi --all-targets --all-features
-D warnings passes.
- cargo test -p web-transport-ffi: 6/6 (4 unit, 1 client-config check,
1 end-to-end echo) under both aws-lc-rs and ring features.
- ./rs/web-transport-ffi/build.sh --bindings-only generates all three
language bindings.
- Python files parse cleanly via ast.parse.
- All four workflow YAMLs validate via python3 -c "yaml.safe_load".
* py/web-transport: green up CI lint / ty / formatting
Three categories of CI failure from PR #239's first run, fixed:
- Ruff was checking the maturin-generated uniffi binding at
python/web_transport/_uniffi/web_transport.py (~3.4k lines) — adds
`extend-exclude` and a .gitignore entry so the vendored generator
output stays out of both lint and the git index.
- ty was reporting 180+ diagnostics: 141 of them
`asyncio.TaskGroup` / `asyncio.timeout` complaints because I had
lowered `requires-python` to >=3.10 (TaskGroup is 3.11+, and the
existing tests use 3.12+ idioms). Bumped back to >=3.12 to match the
legacy contract; the published wheels are already 3.12+3.13 only.
- The remaining 35 ty errors were
`Invalid object caught in an exception handler` on every
`except _uniffi.WebTransportError`. UniFFI generates two classes by
the same name in the bindings — the real Exception subclass (renamed
to a private alias) and a container holding the variant subclasses.
Static checkers resolve the public name to the container, which
isn't an Exception subclass. Expose a `_UniffiError` alias in
`_errors.py` (`= _uniffi.WebTransportError.Connect.__bases__[0]`,
the actual Exception base) and switch all 35 `except` sites to it.
Also reformatted `_errors.py` and `__init__.py` to spaces (matches the
existing `_crypto.py` style); ran ruff format on the rest.
Verification (local):
- `uv run ruff check .` → All checks passed.
- `uv run ruff format --check` → 30/30 files OK.
- `uv run ty check` → All checks passed (0 diagnostics).
- `uv run pytest tests/unit/` → 20/21 pass (the 1 failure is the
sandbox-only IPv6 bind in `test_server_close_code_too_large`; CI
Ubuntu has dual-stack).
- `cargo check --workspace --all-targets --all-features` → green.
- `cargo fmt --all --check` → green.
* Address Python integration + Rust check regressions from CI
Python wrapper (py/web-transport):
- Session.close_reason: was returning a generic SessionClosedLocally on
any close. Added a structured close_reason() FFI method on Rust and
point the wrapper at it, so peer-side closes surface as
SessionClosedByPeer with .source/.code/.reason intact (drove four
test_session_lifecycle / test_client failures).
- Session.close(): validate the u32 range in Python before crossing the
FFI so 2**32 raises OverflowError (legacy contract); without it
uniffi raised ValueError ("u32 requires 0 <= value < 4294967296").
- SendStream/RecvStream/Session __aexit__: await asyncio.sleep(0) /
await wait_closed() so the FFI runtime gets a scheduling tick before
we return. The legacy PyO3 binding made __aexit__ truly async (the
await on the lock yielded implicitly); without an equivalent yield,
the peer's accept_bi/accept_uni never saw the stream before we tore
it down, hanging tests like
test_send_context_manager_resets_on_exception.
- Session.send_datagram: enforce the strict max_datagram_size boundary
in Python. quinn allows datagrams slightly over (up to UDP MTU);
legacy callers expected a hard rejection at max_datagram_size + 1.
Rust FFI:
- New ffi::spawn_abortable helper: wraps tokio::spawn so the spawned
task is aborted when the outer future is dropped (asyncio.Task.cancel
drops the foreign future). Without this, a cancelled read() would
silently keep running on the runtime thread, consume incoming data,
and discard it — the next read() then missed the bytes, breaking
test_asyncio_cancel_read_then_read_again and ..._readexactly_...
- SendStream::cancellable / RecvStream::cancellable switched to it.
- session.close_reason() exposed as a new uniffi method returning the
structured close error.
- SessionRequest::accept/reject now map "request already accepted or
rejected" to WebTransportError::Protocol (matches legacy SessionError
rather than ValueError; what test_double_accept_raises expects).
Rust workspace:
- rs/web-transport-ffi/Cargo.toml: reformatted feature lists to
single-line (cargo sort --workspace --check was failing on the
multi-line aws-lc-rs / ring blocks — caught by `just check`).
- workspace.members got resorted by cargo-sort.
Verification:
- cargo clippy -p web-transport-ffi --all-targets --all-features
-D warnings → clean.
- cargo test -p web-transport-ffi --lib → 6/6 pass.
- cargo sort --workspace --check → clean.
- cargo shear → no unused deps.
- pytest tests/unit/ tests/integration/: 174/174 pass (after reverting
the local IPv6→IPv4 sandbox patch from the test files — CI Ubuntu
has dual-stack so the unchanged [::1] binds work there).
- ruff check / ruff format --check / ty check all clean.
* py/web-transport: stabilize Session.max_datagram_size + ty annotation
Two more Python-suite regressions from the 3.13 CI:
- ty[invalid-assignment]: `Session.close_reason` was annotated as
`WebTransportError | None`, but the legacy browser_interop tests
declare `close_reason: SessionError | None`. Tightened the wrapper
return type back to `SessionError | None` and added a defensive
fallback that wraps any non-session WebTransportError into a
SessionError so the annotation stays accurate at runtime.
- test_datagram_too_large: quinn's `max_datagram_size` grows during
the session as path-MTU discovery learns a larger MTU. The legacy
test pattern `cs.send_datagram(b"x" * max); cs.send_datagram(b"x" *
(max + 1))` reads `max` once, sends a max-sized payload (which bumps
MTU), then sends `max + 1` — but the live max has now grown past
`max + 1`, so the second send succeeds and the test fails. Caching
the initial `max_datagram_size` in the Python wrapper (and using it
for both the public property and the send_datagram boundary check)
pins the value to its session-start reading, matching the contract
the legacy PyO3 binding implied.
Verification on Python 3.13: ruff/ty clean, 174/174 pytest pass.
* release-*.yml: also run on PRs touching FFI/workflow paths
So the next time someone breaks the release pipeline (gradle config,
package.sh, build.sh, maturin args, etc.) it fails the PR instead of
the first real `web-transport-ffi-v*` tag.
Each workflow now triggers on:
push.tags: web-transport-ffi-v* # real release
pull_request.paths: # smoke test
- rs/web-transport-ffi/**
- <language-specific dir>/**
- .github/workflows/release-<lang>.yml
- .github/scripts/release.sh
Per-workflow specifics:
- release-py.yml: parse-version now reads from Cargo.toml on PRs (no
tag exists). verify-version is skipped on PRs (tautological). publish
is gated on `github.event_name != 'pull_request' && vars.PUBLISH_PYTHON`.
- release-kt.yml: same parse-version pattern. The package job runs
end-to-end (gradle assemble + the package.sh staging), but the
`Publish to Maven Central` step is now gated on PR-exclusion too —
PRs from forks can't access MAVEN_CENTRAL_* / SIGNING_* secrets so
the gradle invocation would fail anyway.
- release-swift.yml: same parse-version pattern. The XCFramework still
gets assembled and uploaded as a workflow artifact (so reviewers can
download and sanity-check it), but the release-assets job — which
attaches to a GitHub Release that doesn't exist on a PR — and the
publish stub are both skipped.
Concurrency groups are now keyed by `pull_request.number || github.ref`
so a PR push doesn't cancel a tag-triggered release in flight.
YAML validated via `python3 -c "yaml.safe_load(...)"` on all three.
* kt: unblock release-kt PR smoke (rename Rust field; gate signing)
The new PR-triggered release-kt run caught two real issues in the
Kotlin path that would have only surfaced on the first
`web-transport-ffi-v*` tag without the smoke job:
1. WebTransportError::SessionRejected had a `message: String` field.
UniFFI's Kotlin codegen emits each variant as a class extending the
error parent (which extends Throwable), and Throwable already
defines `val message: String?`. The generated code declares both
the constructor field and an `override val message` getter, so the
Kotlin compiler errors with:
'message' hides member of supertype 'Throwable' and needs an
'override' modifier
plus several "overload resolution ambiguity" follow-ups. Renamed
the field to `detail` on the Rust side (it's only consumed by the
Python wrapper, which I updated in lockstep). No public API change:
the human-readable message still flows through `Exception(...)`.
2. The com.vanniktech.maven.publish plugin's `signAllPublications()`
gates the `publishToMavenLocal` task (transitively, via
`signKotlinMultiplatformPublication`) on a configured signing key.
PR smoke runs don't have SIGNING_KEY set, so gradle aborted with
"no configured signatory" before producing the maven-local layout.
Made `signAllPublications()` conditional on
`ORG_GRADLE_PROJECT_signingInMemoryKey` being present in the env —
real release runs still sign as before (release-kt.yml always sets
that env var before invoking publishAndReleaseToMavenCentral).
Verified locally:
- `cargo test -p web-transport-ffi --lib` → 6/6.
- `uv run ty check` → clean.
- `./rs/web-transport-ffi/build.sh --target x86_64-unknown-linux-gnu`
emits `class SessionRejected(val statusCode, val detail)` in the
generated kotlin (no clash).
- `./kt/scripts/package.sh ...` → BUILD SUCCESSFUL, produces
`release-out/maven-local/dev/moq/web-transport/0.1.0/`.
* kt: inline Android config into build.gradle.kts + classpath the AGP
The PR-triggered release-kt smoke caught a second Kotlin issue: the
package.sh path that runs gradle :web-transport:publishToMavenLocal
with -Pandroid.enabled=true (because per-target Android libs are
present in libs/) failed at script compile time with "Unresolved
reference: kotlin / androidTarget / compileOptions / ndk / namespace
/ ..." — every AGP- or KMP-android-derived symbol came up unresolved.
Root cause: the Android-specific configuration lived in a separate
android.gradle.kts applied via `apply(from = ...)`. Gradle's Kotlin
DSL doesn't generate type-safe accessors for plugins inside
apply-from scripts (the script body is compiled before the imperative
apply runs), so every reference to androidTarget / LibraryExtension /
defaultConfig / ndk / etc. failed to resolve.
Two changes:
1. Inlined the Android block into build.gradle.kts, gated by the same
`androidEnabled` property check. Kotlin DSL now compiles the
accessors correctly because the source file owns both the plugins
block and the configuration.
2. Added `id("com.android.library") version "8.7.3" apply false` to
the plugins block. That puts AGP on the buildscript classpath (so
the Kotlin DSL has the types) without applying it to the project
when `-Pandroid.enabled` isn't set. The conditional
`apply(plugin = "com.android.library")` still gates actual
application — JVM-only contributors (no Google maven access) are
unaffected.
Deleted the now-unused kt/web-transport/android.gradle.kts.
Verified locally:
- ./kt/scripts/package.sh with JVM-only libs → BUILD SUCCESSFUL,
produces dev/moq/web-transport/0.1.0/ in maven-local.
- With Android libs present → gradle gets past the previous
"Unresolved reference" wall and now stops at "SDK location not
found" (no ANDROID_HOME locally); CI's setup-android@v3 step
provides that.
* release-*.yml: cancel-in-progress on PR pushes only
Last push left three release-* workflow runs stuck in `queued` because
the concurrency group serialized them — `cancel-in-progress: false`
was correct for tag-pushed releases (don't kill an in-flight publish)
but wrong for PR smoke tests (a new push should preempt the previous
PR run, not stand in line behind it).
Made `cancel-in-progress` conditional on the trigger:
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
PR pushes now cancel the previous PR run of the same workflow; tag
pushes still queue serially.
Note: this commit is also a retrigger for the python (3.13) failure
on commit 4f8bc88, which only touched Kotlin files. The Python suite
passes locally on 3.13 (174/174, 3 runs in a row), so the CI failure
is suspected flake.
* kt: harden signing-key gate against empty secret
CI hit `Could not read PGP secret key` from the
`signKotlinMultiplatformPublication` task: SIGNING_KEY hadn't been
configured on moq-dev/web-transport yet, so `${{ secrets.SIGNING_KEY }}`
expanded to the empty string. My previous `!= null` gate accepted the
empty env var as "present" and called signAllPublications() against a
key that wasn't really there.
Switched to `!isNullOrBlank()` so the gate only fires when there's an
actual armored block to sign with. Real release runs (SIGNING_KEY set
to the exported `gpg --armor --export-secret-keys ...` output) still
sign as before; PR smoke runs and any "declared but empty" state now
skip signing cleanly.
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent c6c49b6 commit 656ae05
60 files changed
Lines changed: 4077 additions & 2467 deletions
File tree
- .github
- scripts
- workflows
- kt
- scripts
- web-transport
- src
- commonMain/kotlin/dev/moq/webtransport
- jvmAndAndroidMain/kotlin
- jvmMain/resources
- py/web-transport
- python/web_transport
- rs
- web-transport-ffi
- src
- web-transport-python
- src
- swift
- Sources
- WebTransportFFI
- WebTransport
- Tests/WebTransportTests
- scripts
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
0 commit comments