pygrpc-native 0.3.0__tar.gz → 0.4.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.
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/.github/ISSUE_TEMPLATE/config.yml +1 -1
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/.github/workflows/ci.yml +2 -0
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/.pre-commit-config.yaml +1 -0
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/CHANGELOG.md +48 -0
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/Cargo.lock +29 -32
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/Cargo.toml +9 -3
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/PKG-INFO +57 -30
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/README.md +54 -27
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/SECURITY.md +1 -1
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/docs/low-level-api.md +29 -5
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/pyproject.toml +4 -2
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/python/pygrpc_native/_native.pyi +5 -0
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/python/pygrpc_native/_server.py +161 -41
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/python/pygrpc_native/aio.py +190 -16
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/src/lib.rs +64 -12
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/src/metrics.rs +76 -6
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/src/server.rs +226 -34
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/tests/test_aio.py +103 -3
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/tests/test_api.py +3 -0
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/tests/test_calls.py +4 -5
- pygrpc_native-0.4.0/tests/test_connection.py +381 -0
- pygrpc_native-0.4.0/tests/test_downstream.py +386 -0
- pygrpc_native-0.4.0/tests/test_interceptors.py +421 -0
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/tests/test_metrics.py +30 -0
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/tests/test_server.py +1 -3
- pygrpc_native-0.4.0/vendor/http/CHANGELOG.md +269 -0
- pygrpc_native-0.4.0/vendor/http/Cargo.toml +77 -0
- pygrpc_native-0.4.0/vendor/http/LICENSE-APACHE +201 -0
- pygrpc_native-0.4.0/vendor/http/LICENSE-MIT +25 -0
- pygrpc_native-0.4.0/vendor/http/README.md +80 -0
- pygrpc_native-0.4.0/vendor/http/src/byte_str.rs +91 -0
- pygrpc_native-0.4.0/vendor/http/src/convert.rs +17 -0
- pygrpc_native-0.4.0/vendor/http/src/error.rs +167 -0
- pygrpc_native-0.4.0/vendor/http/src/extensions.rs +358 -0
- pygrpc_native-0.4.0/vendor/http/src/header/map.rs +4044 -0
- pygrpc_native-0.4.0/vendor/http/src/header/mod.rs +142 -0
- pygrpc_native-0.4.0/vendor/http/src/header/name.rs +1895 -0
- pygrpc_native-0.4.0/vendor/http/src/header/value.rs +776 -0
- pygrpc_native-0.4.0/vendor/http/src/lib.rs +210 -0
- pygrpc_native-0.4.0/vendor/http/src/method.rs +521 -0
- pygrpc_native-0.4.0/vendor/http/src/request.rs +1072 -0
- pygrpc_native-0.4.0/vendor/http/src/response.rs +781 -0
- pygrpc_native-0.4.0/vendor/http/src/status.rs +596 -0
- pygrpc_native-0.4.0/vendor/http/src/uri/authority.rs +749 -0
- pygrpc_native-0.4.0/vendor/http/src/uri/builder.rs +261 -0
- pygrpc_native-0.4.0/vendor/http/src/uri/mod.rs +1132 -0
- pygrpc_native-0.4.0/vendor/http/src/uri/path.rs +709 -0
- pygrpc_native-0.4.0/vendor/http/src/uri/port.rs +151 -0
- pygrpc_native-0.4.0/vendor/http/src/uri/scheme.rs +361 -0
- pygrpc_native-0.4.0/vendor/http/src/uri/tests.rs +519 -0
- pygrpc_native-0.4.0/vendor/http/src/version.rs +75 -0
- pygrpc_native-0.4.0/vendor/http/tests/header_map.rs +774 -0
- pygrpc_native-0.4.0/vendor/http/tests/header_map_fuzz.rs +365 -0
- pygrpc_native-0.4.0/vendor/http/tests/status_code.rs +82 -0
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/.github/CODEOWNERS +0 -0
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/.github/dependabot.yml +0 -0
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/.github/workflows/dco.yml +0 -0
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/.github/workflows/release.yml +0 -0
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/.github/workflows/stale.yml +0 -0
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/.gitignore +0 -0
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/CODE_OF_CONDUCT.md +0 -0
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/CONTRIBUTING.md +0 -0
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/DCO +0 -0
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/LICENSE +0 -0
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/benchmarks/README.md +0 -0
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/benchmarks/echo.py +0 -0
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/python/pygrpc_native/__init__.py +0 -0
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/python/pygrpc_native/py.typed +0 -0
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/tests/test_add_methods.py +0 -0
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/tests/test_bearer_auth.py +0 -0
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/tests/test_call_details.py +0 -0
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/tests/test_delay_stream_headers.py +0 -0
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/tests/test_fallback.py +0 -0
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/tests/test_front.py +0 -0
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/tests/test_response_metadata.py +0 -0
- {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/tests/test_stream_try_send.py +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
blank_issues_enabled: true
|
|
2
2
|
contact_links:
|
|
3
3
|
- name: Report a security vulnerability
|
|
4
|
-
url: https://github.com/
|
|
4
|
+
url: https://github.com/pygrpc/pygrpc-native/security/advisories/new
|
|
5
5
|
about: Please report security issues privately, not as a public issue.
|
|
@@ -48,6 +48,8 @@ jobs:
|
|
|
48
48
|
- name: Build and install
|
|
49
49
|
run: |
|
|
50
50
|
python -m pip install maturin pytest grpcio grpcio-tools grpcio-health-checking grpcio-reflection uvloop
|
|
51
|
+
# Interceptor libraries that downstream servers use, for tests/test_downstream.py.
|
|
52
|
+
python -m pip install grpc-interceptor opentelemetry-instrumentation-grpc
|
|
51
53
|
maturin build --release --locked --out dist
|
|
52
54
|
python -m pip install dist/*.whl
|
|
53
55
|
- run: pytest -q tests
|
|
@@ -4,6 +4,54 @@ All notable changes are listed here. The project follows
|
|
|
4
4
|
[semantic versioning](https://semver.org); see
|
|
5
5
|
[Compatibility](README.md#compatibility) for what that covers.
|
|
6
6
|
|
|
7
|
+
## 0.4.0 (2026-10-27)
|
|
8
|
+
|
|
9
|
+
`server()` and `aio.server()` now take the interceptors and options grpcio
|
|
10
|
+
servers commonly pass. Code written for 0.3 keeps working; the changes in
|
|
11
|
+
behavior below bring the servers closer to grpcio.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- `server()` and `aio.server()` take grpcio server interceptors. (#39)
|
|
16
|
+
- `server()` and `aio.server()` take the `grpc.keepalive_time_ms`,
|
|
17
|
+
`grpc.keepalive_timeout_ms` and `grpc.keepalive_permit_without_calls`
|
|
18
|
+
options, and `NativeGrpcFront` the matching `keepalive_*` arguments. (#47)
|
|
19
|
+
- `server()` and `aio.server()` take `grpc.max_concurrent_streams`, and
|
|
20
|
+
`NativeGrpcFront` a matching `max_concurrent_streams` argument. (#51)
|
|
21
|
+
- `server()` and `aio.server()` take `grpc.max_metadata_size` and
|
|
22
|
+
`grpc.absolute_max_metadata_size`, and `NativeGrpcFront` a matching
|
|
23
|
+
`max_metadata_size` argument. (#51)
|
|
24
|
+
- `server()` and `aio.server()` ignore options they don't take, as grpcio
|
|
25
|
+
does, such as options only clients read, `grpc.max_message_length` and
|
|
26
|
+
`grpc.http2.max_frame_size`, and values that aren't integers. Only options
|
|
27
|
+
asking for something the server can't do, such as closing connections by
|
|
28
|
+
age or compressing responses, still raise an error. (#47, #51)
|
|
29
|
+
- `metrics()` also reports each call's sent and received message bytes,
|
|
30
|
+
like gRFC A66's `grpc.server.call.sent_total_compressed_message_size` and
|
|
31
|
+
`rcvd_total_compressed_message_size`, under the new keys `size_bounds`,
|
|
32
|
+
`sent` and `rcvd`. (#37)
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- Like grpc-core, the keepalive ping now waits until a connection has
|
|
37
|
+
received nothing for the keepalive time, so busy connections aren't
|
|
38
|
+
pinged. (#47)
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- Handlers that aren't coroutines get grpcio's sync context on
|
|
43
|
+
`aio.server()`, so calling `abort()` from them ends the call with that
|
|
44
|
+
status instead of INTERNAL. (#51)
|
|
45
|
+
- `aio.server()` encodes responses as grpc.aio does: a handler's `None` goes
|
|
46
|
+
out as an empty message when there's no serializer, and a response that
|
|
47
|
+
fails to serialize ends the call with UNKNOWN, like any other error in the
|
|
48
|
+
handler, instead of INTERNAL. (#51)
|
|
49
|
+
- `server()` and `aio.server()` ignore a message size limit that isn't an
|
|
50
|
+
integer, as grpcio does, instead of converting it. (#47)
|
|
51
|
+
- grpcio clients can call a Unix socket server without setting
|
|
52
|
+
`grpc.default_authority`. This uses a patched copy of the `http` crate
|
|
53
|
+
until hyperium/http#836 is fixed upstream. (#35)
|
|
54
|
+
|
|
7
55
|
## 0.3.0 (2026-10-26)
|
|
8
56
|
|
|
9
57
|
Everything here is an addition, so code written for 0.1 and 0.2 keeps working.
|
|
@@ -31,11 +31,11 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
|
|
31
31
|
|
|
32
32
|
[[package]]
|
|
33
33
|
name = "block-buffer"
|
|
34
|
-
version = "0.
|
|
34
|
+
version = "0.12.1"
|
|
35
35
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
36
|
-
checksum = "
|
|
36
|
+
checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
|
|
37
37
|
dependencies = [
|
|
38
|
-
"
|
|
38
|
+
"hybrid-array",
|
|
39
39
|
]
|
|
40
40
|
|
|
41
41
|
[[package]]
|
|
@@ -50,11 +50,17 @@ version = "1.0.5"
|
|
|
50
50
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
51
51
|
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
|
|
52
52
|
|
|
53
|
+
[[package]]
|
|
54
|
+
name = "const-oid"
|
|
55
|
+
version = "0.10.2"
|
|
56
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
57
|
+
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
|
|
58
|
+
|
|
53
59
|
[[package]]
|
|
54
60
|
name = "cpufeatures"
|
|
55
|
-
version = "0.
|
|
61
|
+
version = "0.3.1"
|
|
56
62
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
57
|
-
checksum = "
|
|
63
|
+
checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
|
|
58
64
|
dependencies = [
|
|
59
65
|
"libc",
|
|
60
66
|
]
|
|
@@ -85,21 +91,21 @@ checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"
|
|
|
85
91
|
|
|
86
92
|
[[package]]
|
|
87
93
|
name = "crypto-common"
|
|
88
|
-
version = "0.
|
|
94
|
+
version = "0.2.2"
|
|
89
95
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
90
|
-
checksum = "
|
|
96
|
+
checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
|
|
91
97
|
dependencies = [
|
|
92
|
-
"
|
|
93
|
-
"typenum",
|
|
98
|
+
"hybrid-array",
|
|
94
99
|
]
|
|
95
100
|
|
|
96
101
|
[[package]]
|
|
97
102
|
name = "digest"
|
|
98
|
-
version = "0.
|
|
103
|
+
version = "0.11.3"
|
|
99
104
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
100
|
-
checksum = "
|
|
105
|
+
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
|
|
101
106
|
dependencies = [
|
|
102
107
|
"block-buffer",
|
|
108
|
+
"const-oid",
|
|
103
109
|
"crypto-common",
|
|
104
110
|
]
|
|
105
111
|
|
|
@@ -165,16 +171,6 @@ dependencies = [
|
|
|
165
171
|
"slab",
|
|
166
172
|
]
|
|
167
173
|
|
|
168
|
-
[[package]]
|
|
169
|
-
name = "generic-array"
|
|
170
|
-
version = "0.14.7"
|
|
171
|
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
172
|
-
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
|
173
|
-
dependencies = [
|
|
174
|
-
"typenum",
|
|
175
|
-
"version_check",
|
|
176
|
-
]
|
|
177
|
-
|
|
178
174
|
[[package]]
|
|
179
175
|
name = "h2"
|
|
180
176
|
version = "0.4.20"
|
|
@@ -209,8 +205,6 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
|
|
209
205
|
[[package]]
|
|
210
206
|
name = "http"
|
|
211
207
|
version = "1.5.0"
|
|
212
|
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
213
|
-
checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
|
|
214
208
|
dependencies = [
|
|
215
209
|
"bytes",
|
|
216
210
|
"itoa",
|
|
@@ -251,6 +245,15 @@ version = "1.0.3"
|
|
|
251
245
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
252
246
|
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
|
253
247
|
|
|
248
|
+
[[package]]
|
|
249
|
+
name = "hybrid-array"
|
|
250
|
+
version = "0.4.15"
|
|
251
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
252
|
+
checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17"
|
|
253
|
+
dependencies = [
|
|
254
|
+
"typenum",
|
|
255
|
+
]
|
|
256
|
+
|
|
254
257
|
[[package]]
|
|
255
258
|
name = "hyper"
|
|
256
259
|
version = "1.12.0"
|
|
@@ -392,7 +395,7 @@ dependencies = [
|
|
|
392
395
|
|
|
393
396
|
[[package]]
|
|
394
397
|
name = "pygrpc_native"
|
|
395
|
-
version = "0.
|
|
398
|
+
version = "0.4.0"
|
|
396
399
|
dependencies = [
|
|
397
400
|
"bytes",
|
|
398
401
|
"crossbeam-channel",
|
|
@@ -496,9 +499,9 @@ checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
|
|
496
499
|
|
|
497
500
|
[[package]]
|
|
498
501
|
name = "sha2"
|
|
499
|
-
version = "0.
|
|
502
|
+
version = "0.11.1"
|
|
500
503
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
501
|
-
checksum = "
|
|
504
|
+
checksum = "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19"
|
|
502
505
|
dependencies = [
|
|
503
506
|
"cfg-if",
|
|
504
507
|
"cpufeatures",
|
|
@@ -729,12 +732,6 @@ version = "1.0.27"
|
|
|
729
732
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
730
733
|
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
|
|
731
734
|
|
|
732
|
-
[[package]]
|
|
733
|
-
name = "version_check"
|
|
734
|
-
version = "0.9.5"
|
|
735
|
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
736
|
-
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
|
737
|
-
|
|
738
735
|
[[package]]
|
|
739
736
|
name = "want"
|
|
740
737
|
version = "0.3.2"
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
[package]
|
|
4
4
|
name = "pygrpc_native"
|
|
5
|
-
version = "0.
|
|
5
|
+
version = "0.4.0"
|
|
6
6
|
edition = "2021"
|
|
7
7
|
license = "Apache-2.0"
|
|
8
8
|
description = "A gRPC server in Rust that hands calls to Python handlers in batches"
|
|
9
|
-
repository = "https://github.com/
|
|
9
|
+
repository = "https://github.com/pygrpc/pygrpc-native"
|
|
10
10
|
# tonic 0.14 needs rustc 1.88.
|
|
11
11
|
rust-version = "1.88"
|
|
12
12
|
readme = "README.md"
|
|
@@ -32,7 +32,7 @@ hyper-util = { version = "0.1", features = ["tokio"] }
|
|
|
32
32
|
log = "0.4"
|
|
33
33
|
pyo3 = { version = "0.29", features = ["abi3-py310"] }
|
|
34
34
|
pyo3-log = "0.13"
|
|
35
|
-
sha2 = "0.
|
|
35
|
+
sha2 = "0.11"
|
|
36
36
|
subtle = "2.6"
|
|
37
37
|
tokio = { version = "1", features = ["rt-multi-thread", "net", "sync", "time", "macros"] }
|
|
38
38
|
tonic = { version = "0.14", default-features = false, features = ["server", "gzip"] }
|
|
@@ -41,3 +41,9 @@ tonic = { version = "0.14", default-features = false, features = ["server", "gzi
|
|
|
41
41
|
[profile.release]
|
|
42
42
|
lto = "thin"
|
|
43
43
|
codegen-units = 1
|
|
44
|
+
|
|
45
|
+
# grpc-core sends a Unix socket path, percent-encoded, as :authority, which
|
|
46
|
+
# http 1.5 rejects (hyperium/http#836). The copy allows valid escapes in the
|
|
47
|
+
# host. Drop it once upstream does.
|
|
48
|
+
[patch.crates-io]
|
|
49
|
+
http = { path = "vendor/http" }
|
|
@@ -1,28 +1,33 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pygrpc-native
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Classifier: Programming Language :: Python :: 3
|
|
5
5
|
Classifier: Programming Language :: Rust
|
|
6
6
|
Classifier: Operating System :: POSIX :: Linux
|
|
7
7
|
Classifier: Operating System :: MacOS
|
|
8
8
|
License-File: LICENSE
|
|
9
|
-
Summary: A
|
|
9
|
+
Summary: A drop-in grpcio server whose HTTP/2 and gRPC work runs in Rust, outside the GIL
|
|
10
10
|
License-Expression: Apache-2.0
|
|
11
11
|
Requires-Python: >=3.10
|
|
12
12
|
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
13
|
-
Project-URL: Repository, https://github.com/
|
|
13
|
+
Project-URL: Repository, https://github.com/pygrpc/pygrpc-native
|
|
14
14
|
|
|
15
15
|
# pygrpc-native
|
|
16
16
|
|
|
17
17
|
A gRPC server for Python whose HTTP/2 and gRPC work runs in Rust, outside the
|
|
18
18
|
GIL. Your handlers stay in Python.
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
When a Python gRPC server runs out of CPU, the usual fixes are running more
|
|
21
|
+
server processes or rewriting the servicers in another language. pygrpc-native
|
|
22
|
+
instead moves the gRPC work itself (framing, flow control, deadlines and
|
|
23
|
+
cancellation) onto a tokio runtime outside the GIL, and leaves your servicers,
|
|
24
|
+
interceptors and generated code alone, so Python spends its time on your code.
|
|
25
|
+
`pygrpc_native.server()` and `pygrpc_native.aio.server()` take the same
|
|
26
|
+
arguments as grpcio's servers, so switching is usually a one-line change, and
|
|
27
|
+
nothing changes for clients.
|
|
28
|
+
|
|
29
|
+
Underneath, each call reaches Python as an event: an id, the method, the
|
|
30
|
+
request bytes and who sent it.
|
|
26
31
|
|
|
27
32
|
```
|
|
28
33
|
grpc client ──HTTP/2──▶ tokio threads (no GIL)
|
|
@@ -52,7 +57,7 @@ maturin develop --release
|
|
|
52
57
|
## With grpcio servicers
|
|
53
58
|
|
|
54
59
|
`pygrpc_native.server()` takes the same arguments as `grpc.server()` and
|
|
55
|
-
serves servicers from grpcio-tools generated code (1.
|
|
60
|
+
serves servicers from grpcio-tools generated code (1.64 or later) unchanged:
|
|
56
61
|
|
|
57
62
|
```python
|
|
58
63
|
from concurrent import futures
|
|
@@ -70,15 +75,32 @@ Status codes, error messages, metadata, deadlines, cancellation and
|
|
|
70
75
|
|
|
71
76
|
Unary and server-streaming methods are served natively on one insecure port.
|
|
72
77
|
The rest, such as client-streaming and bidirectional methods, server
|
|
73
|
-
reflection, or handlers added only with `add_generic_rpc_handlers`,
|
|
74
|
-
|
|
75
|
-
faster, and `peer()` in
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
reflection, or handlers added only with `add_generic_rpc_handlers`, as code
|
|
79
|
+
generated by grpcio-tools before 1.64 does, go to a grpcio server on a Unix
|
|
80
|
+
socket behind it. They work, but they aren't any faster, and `peer()` in
|
|
81
|
+
them is the socket.
|
|
82
|
+
|
|
83
|
+
Interceptors run as in grpcio, on the methods served here and on the fallback
|
|
84
|
+
server. Of the server options, the message size limits, the keepalive
|
|
85
|
+
settings (`grpc.keepalive_time_ms`, `grpc.keepalive_timeout_ms` and
|
|
86
|
+
`grpc.keepalive_permit_without_calls`), `grpc.max_concurrent_streams` and the
|
|
87
|
+
metadata size limits (`grpc.max_metadata_size` and
|
|
88
|
+
`grpc.absolute_max_metadata_size`) work as in grpcio. Options that ask for
|
|
89
|
+
something this server can't do raise an error: closing connections by age or
|
|
90
|
+
idleness (`grpc.max_connection_age_ms`, `grpc.max_connection_age_grace_ms`,
|
|
91
|
+
`grpc.max_connection_idle_ms`), `grpc.max_allowed_incoming_connections`,
|
|
92
|
+
response compression and `grpc.so_reuseport`, as do `maximum_concurrent_rpcs`
|
|
93
|
+
and TLS. Any other option is ignored, as grpcio ignores the many that only
|
|
94
|
+
clients read. That covers the limits on client pings, which are accepted but
|
|
95
|
+
not enforced, and `grpc.http2.max_frame_size`, which grpc-core's flow control
|
|
96
|
+
overrides anyway. Values that aren't integers are ignored too, as in grpcio.
|
|
97
|
+
|
|
98
|
+
There are two known differences. If a unary handler calls
|
|
99
|
+
`send_initial_metadata()` and then fails, the metadata goes out with the
|
|
100
|
+
status, so the client sees it as trailing metadata. And a connection whose
|
|
101
|
+
client doesn't answer a keepalive ping is closed after
|
|
102
|
+
`grpc.keepalive_timeout_ms`, as gRPC's keepalive documentation describes,
|
|
103
|
+
while grpcio waits for `grpc.http2.ping_timeout_ms`, a minute by default.
|
|
82
104
|
|
|
83
105
|
`pygrpc_native.aio.server()` does the same for `grpc.aio.server()`:
|
|
84
106
|
|
|
@@ -96,8 +118,9 @@ Handlers run on the event loop that called `start()`, uvloop included.
|
|
|
96
118
|
`abort()` raises `grpc.aio.AbortError`, and a handler whose client cancels or
|
|
97
119
|
whose deadline passes gets `asyncio.CancelledError`, as in grpc.aio. Streams
|
|
98
120
|
can yield from an async generator or call `context.write()`, and handlers
|
|
99
|
-
that aren't coroutines run in `migration_thread_pool
|
|
100
|
-
|
|
121
|
+
that aren't coroutines run in `migration_thread_pool` with grpcio's sync
|
|
122
|
+
context, where `abort()` returns instead of raising. The fallback,
|
|
123
|
+
the unsupported options and the known differences are the same as
|
|
101
124
|
for the sync server.
|
|
102
125
|
|
|
103
126
|
## Low-level API
|
|
@@ -122,7 +145,7 @@ threading.Thread(target=serve).start()
|
|
|
122
145
|
```
|
|
123
146
|
|
|
124
147
|
Streaming, call details, response metadata, bearer tokens and metrics are
|
|
125
|
-
covered in [docs/low-level-api.md](https://github.com/
|
|
148
|
+
covered in [docs/low-level-api.md](https://github.com/pygrpc/pygrpc-native/blob/main/docs/low-level-api.md).
|
|
126
149
|
|
|
127
150
|
## Compatibility
|
|
128
151
|
|
|
@@ -140,22 +163,26 @@ this README. Changes are listed in [CHANGELOG.md](CHANGELOG.md).
|
|
|
140
163
|
- `NativeGrpcFront` serves unary and server-streaming methods only; other
|
|
141
164
|
kinds can go through the fallback server. `server()` and `aio.server()`
|
|
142
165
|
send them there on their own.
|
|
143
|
-
- No TLS yet
|
|
144
|
-
|
|
145
|
-
- Clients of a Unix socket server must set `grpc.default_authority`, since
|
|
146
|
-
the socket path grpc-core sends as `:authority` is not valid HTTP/2.
|
|
166
|
+
- No TLS yet. `NativeGrpcFront` checks bearer tokens itself; `server()` and
|
|
167
|
+
`aio.server()` run grpcio interceptors instead.
|
|
147
168
|
- Like grpc-core, binding a Unix socket path replaces any socket file
|
|
148
169
|
already there.
|
|
149
|
-
-
|
|
150
|
-
|
|
151
|
-
is
|
|
170
|
+
- The server never sets `SO_REUSEPORT`, so binding a port another server is
|
|
171
|
+
using fails, where grpcio's default lets both bind it. `grpc.so_reuseport`
|
|
172
|
+
set to 0 is accepted; other integers raise an error.
|
|
173
|
+
- Per connection: at most 1024 concurrent streams unless
|
|
174
|
+
`grpc.max_concurrent_streams` says otherwise. A connection that hasn't
|
|
175
|
+
finished the HTTP/2 preface after 120 s is dropped.
|
|
176
|
+
- A call with more request metadata than the limit (16 KiB unless set) is
|
|
177
|
+
refused. grpcio also refuses some calls at random between
|
|
178
|
+
`grpc.max_metadata_size` and that limit; this server doesn't.
|
|
152
179
|
|
|
153
180
|
## Benchmarks
|
|
154
181
|
|
|
155
182
|
With an empty handler on an Apple M4 Pro, `pygrpc_native.server()` answers
|
|
156
183
|
about 1.5x the calls per second of `grpc.server()` from one client thread and
|
|
157
184
|
about 5x from 32. On a 40-vCPU Linux container the low-level API answered
|
|
158
|
-
about 12x at 32 threads. See [benchmarks](https://github.com/
|
|
185
|
+
about 12x at 32 threads. See [benchmarks](https://github.com/pygrpc/pygrpc-native/blob/main/benchmarks/README.md) for the
|
|
159
186
|
numbers and how they were measured.
|
|
160
187
|
|
|
161
188
|
## Contributing
|
|
@@ -3,12 +3,17 @@
|
|
|
3
3
|
A gRPC server for Python whose HTTP/2 and gRPC work runs in Rust, outside the
|
|
4
4
|
GIL. Your handlers stay in Python.
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
When a Python gRPC server runs out of CPU, the usual fixes are running more
|
|
7
|
+
server processes or rewriting the servicers in another language. pygrpc-native
|
|
8
|
+
instead moves the gRPC work itself (framing, flow control, deadlines and
|
|
9
|
+
cancellation) onto a tokio runtime outside the GIL, and leaves your servicers,
|
|
10
|
+
interceptors and generated code alone, so Python spends its time on your code.
|
|
11
|
+
`pygrpc_native.server()` and `pygrpc_native.aio.server()` take the same
|
|
12
|
+
arguments as grpcio's servers, so switching is usually a one-line change, and
|
|
13
|
+
nothing changes for clients.
|
|
14
|
+
|
|
15
|
+
Underneath, each call reaches Python as an event: an id, the method, the
|
|
16
|
+
request bytes and who sent it.
|
|
12
17
|
|
|
13
18
|
```
|
|
14
19
|
grpc client ──HTTP/2──▶ tokio threads (no GIL)
|
|
@@ -38,7 +43,7 @@ maturin develop --release
|
|
|
38
43
|
## With grpcio servicers
|
|
39
44
|
|
|
40
45
|
`pygrpc_native.server()` takes the same arguments as `grpc.server()` and
|
|
41
|
-
serves servicers from grpcio-tools generated code (1.
|
|
46
|
+
serves servicers from grpcio-tools generated code (1.64 or later) unchanged:
|
|
42
47
|
|
|
43
48
|
```python
|
|
44
49
|
from concurrent import futures
|
|
@@ -56,15 +61,32 @@ Status codes, error messages, metadata, deadlines, cancellation and
|
|
|
56
61
|
|
|
57
62
|
Unary and server-streaming methods are served natively on one insecure port.
|
|
58
63
|
The rest, such as client-streaming and bidirectional methods, server
|
|
59
|
-
reflection, or handlers added only with `add_generic_rpc_handlers`,
|
|
60
|
-
|
|
61
|
-
faster, and `peer()` in
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
reflection, or handlers added only with `add_generic_rpc_handlers`, as code
|
|
65
|
+
generated by grpcio-tools before 1.64 does, go to a grpcio server on a Unix
|
|
66
|
+
socket behind it. They work, but they aren't any faster, and `peer()` in
|
|
67
|
+
them is the socket.
|
|
68
|
+
|
|
69
|
+
Interceptors run as in grpcio, on the methods served here and on the fallback
|
|
70
|
+
server. Of the server options, the message size limits, the keepalive
|
|
71
|
+
settings (`grpc.keepalive_time_ms`, `grpc.keepalive_timeout_ms` and
|
|
72
|
+
`grpc.keepalive_permit_without_calls`), `grpc.max_concurrent_streams` and the
|
|
73
|
+
metadata size limits (`grpc.max_metadata_size` and
|
|
74
|
+
`grpc.absolute_max_metadata_size`) work as in grpcio. Options that ask for
|
|
75
|
+
something this server can't do raise an error: closing connections by age or
|
|
76
|
+
idleness (`grpc.max_connection_age_ms`, `grpc.max_connection_age_grace_ms`,
|
|
77
|
+
`grpc.max_connection_idle_ms`), `grpc.max_allowed_incoming_connections`,
|
|
78
|
+
response compression and `grpc.so_reuseport`, as do `maximum_concurrent_rpcs`
|
|
79
|
+
and TLS. Any other option is ignored, as grpcio ignores the many that only
|
|
80
|
+
clients read. That covers the limits on client pings, which are accepted but
|
|
81
|
+
not enforced, and `grpc.http2.max_frame_size`, which grpc-core's flow control
|
|
82
|
+
overrides anyway. Values that aren't integers are ignored too, as in grpcio.
|
|
83
|
+
|
|
84
|
+
There are two known differences. If a unary handler calls
|
|
85
|
+
`send_initial_metadata()` and then fails, the metadata goes out with the
|
|
86
|
+
status, so the client sees it as trailing metadata. And a connection whose
|
|
87
|
+
client doesn't answer a keepalive ping is closed after
|
|
88
|
+
`grpc.keepalive_timeout_ms`, as gRPC's keepalive documentation describes,
|
|
89
|
+
while grpcio waits for `grpc.http2.ping_timeout_ms`, a minute by default.
|
|
68
90
|
|
|
69
91
|
`pygrpc_native.aio.server()` does the same for `grpc.aio.server()`:
|
|
70
92
|
|
|
@@ -82,8 +104,9 @@ Handlers run on the event loop that called `start()`, uvloop included.
|
|
|
82
104
|
`abort()` raises `grpc.aio.AbortError`, and a handler whose client cancels or
|
|
83
105
|
whose deadline passes gets `asyncio.CancelledError`, as in grpc.aio. Streams
|
|
84
106
|
can yield from an async generator or call `context.write()`, and handlers
|
|
85
|
-
that aren't coroutines run in `migration_thread_pool
|
|
86
|
-
|
|
107
|
+
that aren't coroutines run in `migration_thread_pool` with grpcio's sync
|
|
108
|
+
context, where `abort()` returns instead of raising. The fallback,
|
|
109
|
+
the unsupported options and the known differences are the same as
|
|
87
110
|
for the sync server.
|
|
88
111
|
|
|
89
112
|
## Low-level API
|
|
@@ -108,7 +131,7 @@ threading.Thread(target=serve).start()
|
|
|
108
131
|
```
|
|
109
132
|
|
|
110
133
|
Streaming, call details, response metadata, bearer tokens and metrics are
|
|
111
|
-
covered in [docs/low-level-api.md](https://github.com/
|
|
134
|
+
covered in [docs/low-level-api.md](https://github.com/pygrpc/pygrpc-native/blob/main/docs/low-level-api.md).
|
|
112
135
|
|
|
113
136
|
## Compatibility
|
|
114
137
|
|
|
@@ -126,22 +149,26 @@ this README. Changes are listed in [CHANGELOG.md](CHANGELOG.md).
|
|
|
126
149
|
- `NativeGrpcFront` serves unary and server-streaming methods only; other
|
|
127
150
|
kinds can go through the fallback server. `server()` and `aio.server()`
|
|
128
151
|
send them there on their own.
|
|
129
|
-
- No TLS yet
|
|
130
|
-
|
|
131
|
-
- Clients of a Unix socket server must set `grpc.default_authority`, since
|
|
132
|
-
the socket path grpc-core sends as `:authority` is not valid HTTP/2.
|
|
152
|
+
- No TLS yet. `NativeGrpcFront` checks bearer tokens itself; `server()` and
|
|
153
|
+
`aio.server()` run grpcio interceptors instead.
|
|
133
154
|
- Like grpc-core, binding a Unix socket path replaces any socket file
|
|
134
155
|
already there.
|
|
135
|
-
-
|
|
136
|
-
|
|
137
|
-
is
|
|
156
|
+
- The server never sets `SO_REUSEPORT`, so binding a port another server is
|
|
157
|
+
using fails, where grpcio's default lets both bind it. `grpc.so_reuseport`
|
|
158
|
+
set to 0 is accepted; other integers raise an error.
|
|
159
|
+
- Per connection: at most 1024 concurrent streams unless
|
|
160
|
+
`grpc.max_concurrent_streams` says otherwise. A connection that hasn't
|
|
161
|
+
finished the HTTP/2 preface after 120 s is dropped.
|
|
162
|
+
- A call with more request metadata than the limit (16 KiB unless set) is
|
|
163
|
+
refused. grpcio also refuses some calls at random between
|
|
164
|
+
`grpc.max_metadata_size` and that limit; this server doesn't.
|
|
138
165
|
|
|
139
166
|
## Benchmarks
|
|
140
167
|
|
|
141
168
|
With an empty handler on an Apple M4 Pro, `pygrpc_native.server()` answers
|
|
142
169
|
about 1.5x the calls per second of `grpc.server()` from one client thread and
|
|
143
170
|
about 5x from 32. On a 40-vCPU Linux container the low-level API answered
|
|
144
|
-
about 12x at 32 threads. See [benchmarks](https://github.com/
|
|
171
|
+
about 12x at 32 threads. See [benchmarks](https://github.com/pygrpc/pygrpc-native/blob/main/benchmarks/README.md) for the
|
|
145
172
|
numbers and how they were measured.
|
|
146
173
|
|
|
147
174
|
## Contributing
|
|
@@ -6,7 +6,7 @@ pull requests.
|
|
|
6
6
|
## Reporting a vulnerability
|
|
7
7
|
|
|
8
8
|
Use **Report a vulnerability** on the
|
|
9
|
-
[Security tab](https://github.com/
|
|
9
|
+
[Security tab](https://github.com/pygrpc/pygrpc-native/security), which
|
|
10
10
|
only the maintainers can see. It helps to include:
|
|
11
11
|
|
|
12
12
|
- the affected version or commit
|
|
@@ -38,7 +38,7 @@ pass the serialized reply to `respond`.
|
|
|
38
38
|
|
|
39
39
|
## Reference
|
|
40
40
|
|
|
41
|
-
`NativeGrpcFront(target, methods, io_threads=2, client_key_metadata=None, call_details=False, delay_stream_headers=False, bearer_token_sha256=None, unauthenticated_message="Unauthenticated", metrics=False)`
|
|
41
|
+
`NativeGrpcFront(target, methods, io_threads=2, client_key_metadata=None, call_details=False, delay_stream_headers=False, bearer_token_sha256=None, unauthenticated_message="Unauthenticated", metrics=False, keepalive_time_ms=7200000, keepalive_timeout_ms=20000, keepalive_permit_without_calls=True, max_concurrent_streams=1024, max_metadata_size=16384)`
|
|
42
42
|
binds `host:port` (every address the host resolves to), `unix:path` or
|
|
43
43
|
`unix:///path`. Port 0 picks a free port. `methods` lists
|
|
44
44
|
`(path, server_streaming)` pairs, where a path is `/package.Service/Method`
|
|
@@ -106,6 +106,20 @@ front = NativeGrpcFront(
|
|
|
106
106
|
)
|
|
107
107
|
```
|
|
108
108
|
|
|
109
|
+
Like grpc-core, the server pings a connection that has received nothing for
|
|
110
|
+
`keepalive_time_ms` and closes it if the client doesn't answer within
|
|
111
|
+
`keepalive_timeout_ms`, which is how it notices a client that went away
|
|
112
|
+
without closing the connection. A client that is still sending gets up to a
|
|
113
|
+
minute to answer. A `keepalive_time_ms` of 2147483647 (INT_MAX, as in
|
|
114
|
+
grpc-core) or more turns the pings off. With `keepalive_permit_without_calls=False`, as grpc-core
|
|
115
|
+
servers do by default, a connection is only pinged while it has calls in
|
|
116
|
+
flight; `server()` and `aio.server()` default to that.
|
|
117
|
+
|
|
118
|
+
`max_concurrent_streams` limits the calls one connection can have open; a
|
|
119
|
+
client waits for one to finish before starting another. `max_metadata_size`
|
|
120
|
+
is the most request metadata, in bytes, a call may carry, counted as HTTP/2
|
|
121
|
+
counts a header list; a call with more is refused.
|
|
122
|
+
|
|
109
123
|
`set_fallback` lets one port serve methods you don't list: run a regular
|
|
110
124
|
`grpc.Server` on a Unix socket and its methods are proxied there, with its
|
|
111
125
|
interceptors.
|
|
@@ -113,9 +127,11 @@ interceptors.
|
|
|
113
127
|
## Metrics
|
|
114
128
|
|
|
115
129
|
With `metrics=True`, the front counts calls like gRFC A66's
|
|
116
|
-
`grpc.server.call.started
|
|
117
|
-
call
|
|
118
|
-
|
|
130
|
+
`grpc.server.call.started`, `grpc.server.call.duration`,
|
|
131
|
+
`grpc.server.call.sent_total_compressed_message_size` and
|
|
132
|
+
`grpc.server.call.rcvd_total_compressed_message_size`. It counts each call
|
|
133
|
+
when it starts, and records its status, duration and message bytes when the
|
|
134
|
+
trailers go out. A call the client resets counts as CANCELLED, so one the client drops at
|
|
119
135
|
its deadline can show up as CANCELLED or DEADLINE_EXCEEDED, depending on which
|
|
120
136
|
side notices first. Calls to paths you didn't list count under `"other"`, so
|
|
121
137
|
unknown paths can't add entries. Calls forwarded to the fallback aren't
|
|
@@ -127,12 +143,15 @@ you call it:
|
|
|
127
143
|
{
|
|
128
144
|
"start_time_unix_nano": 1761436800000000000,
|
|
129
145
|
"bounds": [0.0, 1e-05, ..., 100.0], # A66's latency buckets, in seconds
|
|
146
|
+
"size_bounds": [0, 1024, ..., 4294967296], # A66's size buckets, in bytes
|
|
130
147
|
"methods": {
|
|
131
148
|
"/echo.Echo/Say": {
|
|
132
149
|
"started": 1042, # still running: started minus the counts
|
|
133
150
|
"status": {
|
|
134
151
|
0: {"count": 1040, "sum": 0.21, "min": 0.00009, "max": 0.0031,
|
|
135
|
-
"buckets": [0, 0, 0, 12, ...]
|
|
152
|
+
"buckets": [0, 0, 0, 12, ...], # one more than bounds
|
|
153
|
+
"sent": {"sum": 66560, "buckets": [0, 1040, 0, ...]},
|
|
154
|
+
"rcvd": {"sum": 66560, "buckets": [0, 1040, 0, ...]}},
|
|
136
155
|
},
|
|
137
156
|
},
|
|
138
157
|
},
|
|
@@ -146,3 +165,8 @@ histograms use the same layout, while Prometheus `le` buckets are cumulative,
|
|
|
146
165
|
so add them up from the first one. Nothing resets: the totals count up from
|
|
147
166
|
`start_time_unix_nano`. The status keys are gRPC status code numbers, with 0
|
|
148
167
|
for OK. A66 labels statuses by name instead.
|
|
168
|
+
|
|
169
|
+
`sent` and `rcvd` hold each call's total message bytes, without the 5-byte
|
|
170
|
+
gRPC frame headers, in the same layout over `size_bounds`. A compressed
|
|
171
|
+
request counts at its compressed size, as A66 asks. Responses aren't
|
|
172
|
+
compressed.
|
|
@@ -5,7 +5,7 @@ build-backend = "maturin"
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pygrpc-native"
|
|
7
7
|
dynamic = ["version"]
|
|
8
|
-
description = "A
|
|
8
|
+
description = "A drop-in grpcio server whose HTTP/2 and gRPC work runs in Rust, outside the GIL"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
11
11
|
requires-python = ">=3.10"
|
|
@@ -18,9 +18,11 @@ classifiers = [
|
|
|
18
18
|
]
|
|
19
19
|
|
|
20
20
|
[project.urls]
|
|
21
|
-
Repository = "https://github.com/
|
|
21
|
+
Repository = "https://github.com/pygrpc/pygrpc-native"
|
|
22
22
|
|
|
23
23
|
[tool.maturin]
|
|
24
24
|
bindings = "pyo3"
|
|
25
25
|
python-source = "python"
|
|
26
26
|
module-name = "pygrpc_native._native"
|
|
27
|
+
# The patched http crate (see Cargo.toml) has to ship in the sdist.
|
|
28
|
+
include = [{ path = "vendor/**/*", format = "sdist" }]
|