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.
Files changed (79) hide show
  1. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/.github/ISSUE_TEMPLATE/config.yml +1 -1
  2. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/.github/workflows/ci.yml +2 -0
  3. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/.pre-commit-config.yaml +1 -0
  4. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/CHANGELOG.md +48 -0
  5. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/Cargo.lock +29 -32
  6. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/Cargo.toml +9 -3
  7. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/PKG-INFO +57 -30
  8. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/README.md +54 -27
  9. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/SECURITY.md +1 -1
  10. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/docs/low-level-api.md +29 -5
  11. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/pyproject.toml +4 -2
  12. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/python/pygrpc_native/_native.pyi +5 -0
  13. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/python/pygrpc_native/_server.py +161 -41
  14. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/python/pygrpc_native/aio.py +190 -16
  15. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/src/lib.rs +64 -12
  16. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/src/metrics.rs +76 -6
  17. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/src/server.rs +226 -34
  18. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/tests/test_aio.py +103 -3
  19. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/tests/test_api.py +3 -0
  20. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/tests/test_calls.py +4 -5
  21. pygrpc_native-0.4.0/tests/test_connection.py +381 -0
  22. pygrpc_native-0.4.0/tests/test_downstream.py +386 -0
  23. pygrpc_native-0.4.0/tests/test_interceptors.py +421 -0
  24. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/tests/test_metrics.py +30 -0
  25. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/tests/test_server.py +1 -3
  26. pygrpc_native-0.4.0/vendor/http/CHANGELOG.md +269 -0
  27. pygrpc_native-0.4.0/vendor/http/Cargo.toml +77 -0
  28. pygrpc_native-0.4.0/vendor/http/LICENSE-APACHE +201 -0
  29. pygrpc_native-0.4.0/vendor/http/LICENSE-MIT +25 -0
  30. pygrpc_native-0.4.0/vendor/http/README.md +80 -0
  31. pygrpc_native-0.4.0/vendor/http/src/byte_str.rs +91 -0
  32. pygrpc_native-0.4.0/vendor/http/src/convert.rs +17 -0
  33. pygrpc_native-0.4.0/vendor/http/src/error.rs +167 -0
  34. pygrpc_native-0.4.0/vendor/http/src/extensions.rs +358 -0
  35. pygrpc_native-0.4.0/vendor/http/src/header/map.rs +4044 -0
  36. pygrpc_native-0.4.0/vendor/http/src/header/mod.rs +142 -0
  37. pygrpc_native-0.4.0/vendor/http/src/header/name.rs +1895 -0
  38. pygrpc_native-0.4.0/vendor/http/src/header/value.rs +776 -0
  39. pygrpc_native-0.4.0/vendor/http/src/lib.rs +210 -0
  40. pygrpc_native-0.4.0/vendor/http/src/method.rs +521 -0
  41. pygrpc_native-0.4.0/vendor/http/src/request.rs +1072 -0
  42. pygrpc_native-0.4.0/vendor/http/src/response.rs +781 -0
  43. pygrpc_native-0.4.0/vendor/http/src/status.rs +596 -0
  44. pygrpc_native-0.4.0/vendor/http/src/uri/authority.rs +749 -0
  45. pygrpc_native-0.4.0/vendor/http/src/uri/builder.rs +261 -0
  46. pygrpc_native-0.4.0/vendor/http/src/uri/mod.rs +1132 -0
  47. pygrpc_native-0.4.0/vendor/http/src/uri/path.rs +709 -0
  48. pygrpc_native-0.4.0/vendor/http/src/uri/port.rs +151 -0
  49. pygrpc_native-0.4.0/vendor/http/src/uri/scheme.rs +361 -0
  50. pygrpc_native-0.4.0/vendor/http/src/uri/tests.rs +519 -0
  51. pygrpc_native-0.4.0/vendor/http/src/version.rs +75 -0
  52. pygrpc_native-0.4.0/vendor/http/tests/header_map.rs +774 -0
  53. pygrpc_native-0.4.0/vendor/http/tests/header_map_fuzz.rs +365 -0
  54. pygrpc_native-0.4.0/vendor/http/tests/status_code.rs +82 -0
  55. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/.github/CODEOWNERS +0 -0
  56. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  57. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  58. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  59. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/.github/dependabot.yml +0 -0
  60. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/.github/workflows/dco.yml +0 -0
  61. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/.github/workflows/release.yml +0 -0
  62. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/.github/workflows/stale.yml +0 -0
  63. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/.gitignore +0 -0
  64. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/CODE_OF_CONDUCT.md +0 -0
  65. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/CONTRIBUTING.md +0 -0
  66. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/DCO +0 -0
  67. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/LICENSE +0 -0
  68. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/benchmarks/README.md +0 -0
  69. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/benchmarks/echo.py +0 -0
  70. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/python/pygrpc_native/__init__.py +0 -0
  71. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/python/pygrpc_native/py.typed +0 -0
  72. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/tests/test_add_methods.py +0 -0
  73. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/tests/test_bearer_auth.py +0 -0
  74. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/tests/test_call_details.py +0 -0
  75. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/tests/test_delay_stream_headers.py +0 -0
  76. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/tests/test_fallback.py +0 -0
  77. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/tests/test_front.py +0 -0
  78. {pygrpc_native-0.3.0 → pygrpc_native-0.4.0}/tests/test_response_metadata.py +0 -0
  79. {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/theminjunchoi/pygrpc-native/security/advisories/new
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
@@ -1,3 +1,4 @@
1
+ exclude: ^vendor/
1
2
  repos:
2
3
  - repo: https://github.com/astral-sh/ruff-pre-commit
3
4
  rev: v0.11.7
@@ -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.10.4"
34
+ version = "0.12.1"
35
35
  source = "registry+https://github.com/rust-lang/crates.io-index"
36
- checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
36
+ checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
37
37
  dependencies = [
38
- "generic-array",
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.2.17"
61
+ version = "0.3.1"
56
62
  source = "registry+https://github.com/rust-lang/crates.io-index"
57
- checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
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.1.7"
94
+ version = "0.2.2"
89
95
  source = "registry+https://github.com/rust-lang/crates.io-index"
90
- checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
96
+ checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
91
97
  dependencies = [
92
- "generic-array",
93
- "typenum",
98
+ "hybrid-array",
94
99
  ]
95
100
 
96
101
  [[package]]
97
102
  name = "digest"
98
- version = "0.10.7"
103
+ version = "0.11.3"
99
104
  source = "registry+https://github.com/rust-lang/crates.io-index"
100
- checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
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.3.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.10.9"
502
+ version = "0.11.1"
500
503
  source = "registry+https://github.com/rust-lang/crates.io-index"
501
- checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
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.3.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/theminjunchoi/pygrpc-native"
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.10"
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.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 gRPC server in Rust that hands calls to Python handlers in batches
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/theminjunchoi/pygrpc-native
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
- grpcio runs its own Python code for every call, under the same GIL as your
21
- handlers. pygrpc-native does the framing, flow control, deadlines and
22
- cancellation on a tokio runtime instead, and gives Python each call as an
23
- event: an id, the method, the request bytes and who sent it. You decode the
24
- protobuf, run the handler and send back the encoded response. Clients and the
25
- wire format are the same as with grpcio.
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.62 or later) unchanged:
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`, go to a
74
- grpcio server on a Unix socket behind it. They work, but they aren't any
75
- faster, and `peer()` in them is the socket.
76
-
77
- Interceptors, `maximum_concurrent_rpcs`, TLS, response compression, and server
78
- options other than the message size limits and client ping settings raise an
79
- error rather than being ignored. There's one known difference: if a unary
80
- handler calls `send_initial_metadata()` and then fails, the metadata goes out
81
- with the status, so the client sees it as trailing metadata.
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`. The fallback,
100
- the unsupported options and the unary metadata difference are the same as
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/theminjunchoi/pygrpc-native/blob/main/docs/low-level-api.md).
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, and grpcio interceptors don't run on the listed methods. Bearer
144
- tokens are the only authentication, and only on `NativeGrpcFront`.
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
- - Per connection: at most 1024 concurrent streams and 16 KiB of request
150
- metadata. A connection that hasn't finished the HTTP/2 preface after 120 s
151
- is dropped.
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/theminjunchoi/pygrpc-native/blob/main/benchmarks/README.md) for the
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
- grpcio runs its own Python code for every call, under the same GIL as your
7
- handlers. pygrpc-native does the framing, flow control, deadlines and
8
- cancellation on a tokio runtime instead, and gives Python each call as an
9
- event: an id, the method, the request bytes and who sent it. You decode the
10
- protobuf, run the handler and send back the encoded response. Clients and the
11
- wire format are the same as with grpcio.
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.62 or later) unchanged:
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`, go to a
60
- grpcio server on a Unix socket behind it. They work, but they aren't any
61
- faster, and `peer()` in them is the socket.
62
-
63
- Interceptors, `maximum_concurrent_rpcs`, TLS, response compression, and server
64
- options other than the message size limits and client ping settings raise an
65
- error rather than being ignored. There's one known difference: if a unary
66
- handler calls `send_initial_metadata()` and then fails, the metadata goes out
67
- with the status, so the client sees it as trailing metadata.
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`. The fallback,
86
- the unsupported options and the unary metadata difference are the same as
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/theminjunchoi/pygrpc-native/blob/main/docs/low-level-api.md).
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, and grpcio interceptors don't run on the listed methods. Bearer
130
- tokens are the only authentication, and only on `NativeGrpcFront`.
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
- - Per connection: at most 1024 concurrent streams and 16 KiB of request
136
- metadata. A connection that hasn't finished the HTTP/2 preface after 120 s
137
- is dropped.
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/theminjunchoi/pygrpc-native/blob/main/benchmarks/README.md) for the
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/theminjunchoi/pygrpc-native/security), which
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` and `grpc.server.call.duration`. It counts each
117
- call when it starts, and records its status and duration when the trailers go
118
- out. A call the client resets counts as CANCELLED, so one the client drops at
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, ...]}, # one more than bounds
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 gRPC server in Rust that hands calls to Python handlers in batches"
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/theminjunchoi/pygrpc-native"
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" }]