diff --git a/README.md b/README.md index 29f346a70..94d40c33e 100644 --- a/README.md +++ b/README.md @@ -536,6 +536,12 @@ protoc \ /usr/local/include/google/protobuf/*.proto ``` +### Using grpcio library instead of grpclib + +In order to use the `grpcio` library instead of `grpclib`, you can use the `--custom_opt=grpcio` +option when running the `protoc` command. +This will generate stubs compatible with the `grpcio` library. + ### TODO - [x] Fixed length fields diff --git a/poetry.lock b/poetry.lock index 374226890..fe9ce6179 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. [[package]] name = "alabaster" @@ -121,7 +121,7 @@ dev = ["isort (>=5.11.5)", "ruff"] doc = ["sphinx", "sphinx-bootstrap-theme"] hg = ["python-hglib"] plugs = ["asv-bench-memray"] -test = ["feedparser", "filelock", "flaky", "numpy", "pytest", "pytest-rerunfailures", "pytest-rerunfailures (>=10.0)", "pytest-timeout", "pytest-xdist", "python-hglib", "rpy2", "scipy", "selenium", "virtualenv"] +test = ["feedparser", "filelock", "flaky", "numpy", "pytest", "pytest-rerunfailures", "pytest-rerunfailures (>=10.0)", "pytest-timeout", "pytest-xdist", "python-hglib ; platform_system != \"Windows\"", "rpy2 ; platform_system != \"Windows\" and platform_python_implementation != \"PyPy\"", "scipy ; platform_python_implementation != \"PyPy\"", "selenium", "virtualenv"] virtualenv = ["packaging", "virtualenv"] [[package]] @@ -164,6 +164,7 @@ description = "Fast conversion between betterproto messages and Protobuf wire fo optional = true python-versions = ">=3.7" groups = ["main"] +markers = "extra == \"rust-codec\"" files = [ {file = "betterproto_rust_codec-0.1.1-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:38ec2ec1743d815a04ffc020e8e3791955601b239b097e4ae0721528d4d8b608"}, {file = "betterproto_rust_codec-0.1.1-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:96a6deef8cda4b4d084df98b621e39a3123d8878dab551b86bbe733d885c4965"}, @@ -242,7 +243,7 @@ tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} [package.extras] docs = ["furo (>=2023.08.17)", "sphinx (>=7.0,<8.0)", "sphinx-argparse-cli (>=1.5)", "sphinx-autodoc-typehints (>=1.10)", "sphinx-issues (>=3.0.0)"] -test = ["build[uv,virtualenv]", "filelock (>=3)", "pytest (>=6.2.4)", "pytest-cov (>=2.12)", "pytest-mock (>=2)", "pytest-rerunfailures (>=9.1)", "pytest-xdist (>=1.34)", "setuptools (>=42.0.0)", "setuptools (>=56.0.0)", "setuptools (>=56.0.0)", "setuptools (>=67.8.0)", "wheel (>=0.36.0)"] +test = ["build[uv,virtualenv]", "filelock (>=3)", "pytest (>=6.2.4)", "pytest-cov (>=2.12)", "pytest-mock (>=2)", "pytest-rerunfailures (>=9.1)", "pytest-xdist (>=1.34)", "setuptools (>=42.0.0) ; python_version < \"3.10\"", "setuptools (>=56.0.0) ; python_version == \"3.10\"", "setuptools (>=56.0.0) ; python_version == \"3.11\"", "setuptools (>=67.8.0) ; python_version >= \"3.12\"", "wheel (>=0.36.0)"] typing = ["build[uv]", "importlib-metadata (>=5.1)", "mypy (>=1.9.0,<1.10.0)", "tomli", "typing-extensions (>=3.7.4.3)"] uv = ["uv (>=0.1.18)"] virtualenv = ["virtualenv (>=20.0.35)"] @@ -498,7 +499,7 @@ files = [ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} [package.extras] -toml = ["tomli"] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "curtsies" @@ -617,7 +618,7 @@ files = [ [package.extras] docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] -typing = ["typing-extensions (>=4.12.2)"] +typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] [[package]] name = "greenlet" @@ -708,71 +709,84 @@ test = ["objgraph", "psutil"] [[package]] name = "grpcio" -version = "1.69.0" +version = "1.73.0" description = "HTTP/2-based RPC framework" optional = false -python-versions = ">=3.8" -groups = ["dev"] +python-versions = ">=3.9" +groups = ["main", "dev", "test"] files = [ - {file = "grpcio-1.69.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:2060ca95a8db295ae828d0fc1c7f38fb26ccd5edf9aa51a0f44251f5da332e97"}, - {file = "grpcio-1.69.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:2e52e107261fd8fa8fa457fe44bfadb904ae869d87c1280bf60f93ecd3e79278"}, - {file = "grpcio-1.69.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:316463c0832d5fcdb5e35ff2826d9aa3f26758d29cdfb59a368c1d6c39615a11"}, - {file = "grpcio-1.69.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:26c9a9c4ac917efab4704b18eed9082ed3b6ad19595f047e8173b5182fec0d5e"}, - {file = "grpcio-1.69.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90b3646ced2eae3a0599658eeccc5ba7f303bf51b82514c50715bdd2b109e5ec"}, - {file = "grpcio-1.69.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3b75aea7c6cb91b341c85e7c1d9db1e09e1dd630b0717f836be94971e015031e"}, - {file = "grpcio-1.69.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5cfd14175f9db33d4b74d63de87c64bb0ee29ce475ce3c00c01ad2a3dc2a9e51"}, - {file = "grpcio-1.69.0-cp310-cp310-win32.whl", hash = "sha256:9031069d36cb949205293cf0e243abd5e64d6c93e01b078c37921493a41b72dc"}, - {file = "grpcio-1.69.0-cp310-cp310-win_amd64.whl", hash = "sha256:cc89b6c29f3dccbe12d7a3b3f1b3999db4882ae076c1c1f6df231d55dbd767a5"}, - {file = "grpcio-1.69.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:8de1b192c29b8ce45ee26a700044717bcbbd21c697fa1124d440548964328561"}, - {file = "grpcio-1.69.0-cp311-cp311-macosx_10_14_universal2.whl", hash = "sha256:7e76accf38808f5c5c752b0ab3fd919eb14ff8fafb8db520ad1cc12afff74de6"}, - {file = "grpcio-1.69.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:d5658c3c2660417d82db51e168b277e0ff036d0b0f859fa7576c0ffd2aec1442"}, - {file = "grpcio-1.69.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5494d0e52bf77a2f7eb17c6da662886ca0a731e56c1c85b93505bece8dc6cf4c"}, - {file = "grpcio-1.69.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ed866f9edb574fd9be71bf64c954ce1b88fc93b2a4cbf94af221e9426eb14d6"}, - {file = "grpcio-1.69.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c5ba38aeac7a2fe353615c6b4213d1fbb3a3c34f86b4aaa8be08baaaee8cc56d"}, - {file = "grpcio-1.69.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f79e05f5bbf551c4057c227d1b041ace0e78462ac8128e2ad39ec58a382536d2"}, - {file = "grpcio-1.69.0-cp311-cp311-win32.whl", hash = "sha256:bf1f8be0da3fcdb2c1e9f374f3c2d043d606d69f425cd685110dd6d0d2d61258"}, - {file = "grpcio-1.69.0-cp311-cp311-win_amd64.whl", hash = "sha256:fb9302afc3a0e4ba0b225cd651ef8e478bf0070cf11a529175caecd5ea2474e7"}, - {file = "grpcio-1.69.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:fc18a4de8c33491ad6f70022af5c460b39611e39578a4d84de0fe92f12d5d47b"}, - {file = "grpcio-1.69.0-cp312-cp312-macosx_10_14_universal2.whl", hash = "sha256:0f0270bd9ffbff6961fe1da487bdcd594407ad390cc7960e738725d4807b18c4"}, - {file = "grpcio-1.69.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:dc48f99cc05e0698e689b51a05933253c69a8c8559a47f605cff83801b03af0e"}, - {file = "grpcio-1.69.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e925954b18d41aeb5ae250262116d0970893b38232689c4240024e4333ac084"}, - {file = "grpcio-1.69.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87d222569273720366f68a99cb62e6194681eb763ee1d3b1005840678d4884f9"}, - {file = "grpcio-1.69.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b62b0f41e6e01a3e5082000b612064c87c93a49b05f7602fe1b7aa9fd5171a1d"}, - {file = "grpcio-1.69.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:db6f9fd2578dbe37db4b2994c94a1d9c93552ed77dca80e1657bb8a05b898b55"}, - {file = "grpcio-1.69.0-cp312-cp312-win32.whl", hash = "sha256:b192b81076073ed46f4b4dd612b8897d9a1e39d4eabd822e5da7b38497ed77e1"}, - {file = "grpcio-1.69.0-cp312-cp312-win_amd64.whl", hash = "sha256:1227ff7836f7b3a4ab04e5754f1d001fa52a730685d3dc894ed8bc262cc96c01"}, - {file = "grpcio-1.69.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:a78a06911d4081a24a1761d16215a08e9b6d4d29cdbb7e427e6c7e17b06bcc5d"}, - {file = "grpcio-1.69.0-cp313-cp313-macosx_10_14_universal2.whl", hash = "sha256:dc5a351927d605b2721cbb46158e431dd49ce66ffbacb03e709dc07a491dde35"}, - {file = "grpcio-1.69.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:3629d8a8185f5139869a6a17865d03113a260e311e78fbe313f1a71603617589"}, - {file = "grpcio-1.69.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9a281878feeb9ae26db0622a19add03922a028d4db684658f16d546601a4870"}, - {file = "grpcio-1.69.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cc614e895177ab7e4b70f154d1a7c97e152577ea101d76026d132b7aaba003b"}, - {file = "grpcio-1.69.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:1ee76cd7e2e49cf9264f6812d8c9ac1b85dda0eaea063af07292400f9191750e"}, - {file = "grpcio-1.69.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:0470fa911c503af59ec8bc4c82b371ee4303ececbbdc055f55ce48e38b20fd67"}, - {file = "grpcio-1.69.0-cp313-cp313-win32.whl", hash = "sha256:b650f34aceac8b2d08a4c8d7dc3e8a593f4d9e26d86751ebf74ebf5107d927de"}, - {file = "grpcio-1.69.0-cp313-cp313-win_amd64.whl", hash = "sha256:028337786f11fecb5d7b7fa660475a06aabf7e5e52b5ac2df47414878c0ce7ea"}, - {file = "grpcio-1.69.0-cp38-cp38-linux_armv7l.whl", hash = "sha256:b7f693db593d6bf285e015d5538bf1c86cf9c60ed30b6f7da04a00ed052fe2f3"}, - {file = "grpcio-1.69.0-cp38-cp38-macosx_10_14_universal2.whl", hash = "sha256:8b94e83f66dbf6fd642415faca0608590bc5e8d30e2c012b31d7d1b91b1de2fd"}, - {file = "grpcio-1.69.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:b634851b92c090763dde61df0868c730376cdb73a91bcc821af56ae043b09596"}, - {file = "grpcio-1.69.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bf5f680d3ed08c15330d7830d06bc65f58ca40c9999309517fd62880d70cb06e"}, - {file = "grpcio-1.69.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:200e48a6e7b00f804cf00a1c26292a5baa96507c7749e70a3ec10ca1a288936e"}, - {file = "grpcio-1.69.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:45a4704339b6e5b24b0e136dea9ad3815a94f30eb4f1e1d44c4ac484ef11d8dd"}, - {file = "grpcio-1.69.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:85d347cb8237751b23539981dbd2d9d8f6e9ff90082b427b13022b948eb6347a"}, - {file = "grpcio-1.69.0-cp38-cp38-win32.whl", hash = "sha256:60e5de105dc02832dc8f120056306d0ef80932bcf1c0e2b4ca3b676de6dc6505"}, - {file = "grpcio-1.69.0-cp38-cp38-win_amd64.whl", hash = "sha256:282f47d0928e40f25d007f24eb8fa051cb22551e3c74b8248bc9f9bea9c35fe0"}, - {file = "grpcio-1.69.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:dd034d68a2905464c49479b0c209c773737a4245d616234c79c975c7c90eca03"}, - {file = "grpcio-1.69.0-cp39-cp39-macosx_10_14_universal2.whl", hash = "sha256:01f834732c22a130bdf3dc154d1053bdbc887eb3ccb7f3e6285cfbfc33d9d5cc"}, - {file = "grpcio-1.69.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:a7f4ed0dcf202a70fe661329f8874bc3775c14bb3911d020d07c82c766ce0eb1"}, - {file = "grpcio-1.69.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd7ea241b10bc5f0bb0f82c0d7896822b7ed122b3ab35c9851b440c1ccf81588"}, - {file = "grpcio-1.69.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f03dc9b4da4c0dc8a1db7a5420f575251d7319b7a839004d8916257ddbe4816"}, - {file = "grpcio-1.69.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ca71d73a270dff052fe4edf74fef142d6ddd1f84175d9ac4a14b7280572ac519"}, - {file = "grpcio-1.69.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ccbed100dc43704e94ccff9e07680b540d64e4cc89213ab2832b51b4f68a520"}, - {file = "grpcio-1.69.0-cp39-cp39-win32.whl", hash = "sha256:1514341def9c6ec4b7f0b9628be95f620f9d4b99331b7ef0a1845fd33d9b579c"}, - {file = "grpcio-1.69.0-cp39-cp39-win_amd64.whl", hash = "sha256:c1fea55d26d647346acb0069b08dca70984101f2dc95066e003019207212e303"}, - {file = "grpcio-1.69.0.tar.gz", hash = "sha256:936fa44241b5379c5afc344e1260d467bee495747eaf478de825bab2791da6f5"}, -] + {file = "grpcio-1.73.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:d050197eeed50f858ef6c51ab09514856f957dba7b1f7812698260fc9cc417f6"}, + {file = "grpcio-1.73.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:ebb8d5f4b0200916fb292a964a4d41210de92aba9007e33d8551d85800ea16cb"}, + {file = "grpcio-1.73.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:c0811331b469e3f15dda5f90ab71bcd9681189a83944fd6dc908e2c9249041ef"}, + {file = "grpcio-1.73.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12787c791c3993d0ea1cc8bf90393647e9a586066b3b322949365d2772ba965b"}, + {file = "grpcio-1.73.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c17771e884fddf152f2a0df12478e8d02853e5b602a10a9a9f1f52fa02b1d32"}, + {file = "grpcio-1.73.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:275e23d4c428c26b51857bbd95fcb8e528783597207ec592571e4372b300a29f"}, + {file = "grpcio-1.73.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9ffc972b530bf73ef0f948f799482a1bf12d9b6f33406a8e6387c0ca2098a833"}, + {file = "grpcio-1.73.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ebd8d269df64aff092b2cec5e015d8ae09c7e90888b5c35c24fdca719a2c9f35"}, + {file = "grpcio-1.73.0-cp310-cp310-win32.whl", hash = "sha256:072d8154b8f74300ed362c01d54af8b93200c1a9077aeaea79828d48598514f1"}, + {file = "grpcio-1.73.0-cp310-cp310-win_amd64.whl", hash = "sha256:ce953d9d2100e1078a76a9dc2b7338d5415924dc59c69a15bf6e734db8a0f1ca"}, + {file = "grpcio-1.73.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:51036f641f171eebe5fa7aaca5abbd6150f0c338dab3a58f9111354240fe36ec"}, + {file = "grpcio-1.73.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:d12bbb88381ea00bdd92c55aff3da3391fd85bc902c41275c8447b86f036ce0f"}, + {file = "grpcio-1.73.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:483c507c2328ed0e01bc1adb13d1eada05cc737ec301d8e5a8f4a90f387f1790"}, + {file = "grpcio-1.73.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c201a34aa960c962d0ce23fe5f423f97e9d4b518ad605eae6d0a82171809caaa"}, + {file = "grpcio-1.73.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:859f70c8e435e8e1fa060e04297c6818ffc81ca9ebd4940e180490958229a45a"}, + {file = "grpcio-1.73.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e2459a27c6886e7e687e4e407778425f3c6a971fa17a16420227bda39574d64b"}, + {file = "grpcio-1.73.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e0084d4559ee3dbdcce9395e1bc90fdd0262529b32c417a39ecbc18da8074ac7"}, + {file = "grpcio-1.73.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef5fff73d5f724755693a464d444ee0a448c6cdfd3c1616a9223f736c622617d"}, + {file = "grpcio-1.73.0-cp311-cp311-win32.whl", hash = "sha256:965a16b71a8eeef91fc4df1dc40dc39c344887249174053814f8a8e18449c4c3"}, + {file = "grpcio-1.73.0-cp311-cp311-win_amd64.whl", hash = "sha256:b71a7b4483d1f753bbc11089ff0f6fa63b49c97a9cc20552cded3fcad466d23b"}, + {file = "grpcio-1.73.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:fb9d7c27089d9ba3746f18d2109eb530ef2a37452d2ff50f5a6696cd39167d3b"}, + {file = "grpcio-1.73.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:128ba2ebdac41e41554d492b82c34586a90ebd0766f8ebd72160c0e3a57b9155"}, + {file = "grpcio-1.73.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:068ecc415f79408d57a7f146f54cdf9f0acb4b301a52a9e563973dc981e82f3d"}, + {file = "grpcio-1.73.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ddc1cfb2240f84d35d559ade18f69dcd4257dbaa5ba0de1a565d903aaab2968"}, + {file = "grpcio-1.73.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e53007f70d9783f53b41b4cf38ed39a8e348011437e4c287eee7dd1d39d54b2f"}, + {file = "grpcio-1.73.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4dd8d8d092efede7d6f48d695ba2592046acd04ccf421436dd7ed52677a9ad29"}, + {file = "grpcio-1.73.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:70176093d0a95b44d24baa9c034bb67bfe2b6b5f7ebc2836f4093c97010e17fd"}, + {file = "grpcio-1.73.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:085ebe876373ca095e24ced95c8f440495ed0b574c491f7f4f714ff794bbcd10"}, + {file = "grpcio-1.73.0-cp312-cp312-win32.whl", hash = "sha256:cfc556c1d6aef02c727ec7d0016827a73bfe67193e47c546f7cadd3ee6bf1a60"}, + {file = "grpcio-1.73.0-cp312-cp312-win_amd64.whl", hash = "sha256:bbf45d59d090bf69f1e4e1594832aaf40aa84b31659af3c5e2c3f6a35202791a"}, + {file = "grpcio-1.73.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:da1d677018ef423202aca6d73a8d3b2cb245699eb7f50eb5f74cae15a8e1f724"}, + {file = "grpcio-1.73.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:36bf93f6a657f37c131d9dd2c391b867abf1426a86727c3575393e9e11dadb0d"}, + {file = "grpcio-1.73.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:d84000367508ade791d90c2bafbd905574b5ced8056397027a77a215d601ba15"}, + {file = "grpcio-1.73.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c98ba1d928a178ce33f3425ff823318040a2b7ef875d30a0073565e5ceb058d9"}, + {file = "grpcio-1.73.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a73c72922dfd30b396a5f25bb3a4590195ee45ecde7ee068acb0892d2900cf07"}, + {file = "grpcio-1.73.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:10e8edc035724aba0346a432060fd192b42bd03675d083c01553cab071a28da5"}, + {file = "grpcio-1.73.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:f5cdc332b503c33b1643b12ea933582c7b081957c8bc2ea4cc4bc58054a09288"}, + {file = "grpcio-1.73.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:07ad7c57233c2109e4ac999cb9c2710c3b8e3f491a73b058b0ce431f31ed8145"}, + {file = "grpcio-1.73.0-cp313-cp313-win32.whl", hash = "sha256:0eb5df4f41ea10bda99a802b2a292d85be28958ede2a50f2beb8c7fc9a738419"}, + {file = "grpcio-1.73.0-cp313-cp313-win_amd64.whl", hash = "sha256:38cf518cc54cd0c47c9539cefa8888549fcc067db0b0c66a46535ca8032020c4"}, + {file = "grpcio-1.73.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:1284850607901cfe1475852d808e5a102133461ec9380bc3fc9ebc0686ee8e32"}, + {file = "grpcio-1.73.0-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:0e092a4b28eefb63eec00d09ef33291cd4c3a0875cde29aec4d11d74434d222c"}, + {file = "grpcio-1.73.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:33577fe7febffe8ebad458744cfee8914e0c10b09f0ff073a6b149a84df8ab8f"}, + {file = "grpcio-1.73.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:60813d8a16420d01fa0da1fc7ebfaaa49a7e5051b0337cd48f4f950eb249a08e"}, + {file = "grpcio-1.73.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a9c957dc65e5d474378d7bcc557e9184576605d4b4539e8ead6e351d7ccce20"}, + {file = "grpcio-1.73.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3902b71407d021163ea93c70c8531551f71ae742db15b66826cf8825707d2908"}, + {file = "grpcio-1.73.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:1dd7fa7276dcf061e2d5f9316604499eea06b1b23e34a9380572d74fe59915a8"}, + {file = "grpcio-1.73.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2d1510c4ea473110cb46a010555f2c1a279d1c256edb276e17fa571ba1e8927c"}, + {file = "grpcio-1.73.0-cp39-cp39-win32.whl", hash = "sha256:d0a1517b2005ba1235a1190b98509264bf72e231215dfeef8db9a5a92868789e"}, + {file = "grpcio-1.73.0-cp39-cp39-win_amd64.whl", hash = "sha256:6228f7eb6d9f785f38b589d49957fca5df3d5b5349e77d2d89b14e390165344c"}, + {file = "grpcio-1.73.0.tar.gz", hash = "sha256:3af4c30918a7f0d39de500d11255f8d9da4f30e94a2033e70fe2a720e184bd8e"}, +] +markers = {main = "extra == \"grpcio\""} [package.extras] -protobuf = ["grpcio-tools (>=1.69.0)"] +protobuf = ["grpcio-tools (>=1.73.0)"] + +[[package]] +name = "grpcio-testing" +version = "1.71.2" +description = "Testing utilities for gRPC Python" +optional = false +python-versions = "*" +groups = ["test"] +files = [ + {file = "grpcio_testing-1.71.2-py3-none-any.whl", hash = "sha256:1ece9305afc20eeeb7b973a9a390a379ee03a08656a9eeae14646ba82b483963"}, + {file = "grpcio_testing-1.71.2.tar.gz", hash = "sha256:8f2388cec1a94cedb4bffdbe7f3aad3e3a22cd1bcbfe16a2b3ec091ddd2b4fee"}, +] + +[package.dependencies] +grpcio = ">=1.71.2" +protobuf = ">=5.26.1,<6.0dev" [[package]] name = "grpcio-tools" @@ -960,12 +974,12 @@ files = [ zipp = ">=3.20" [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] perf = ["ipython"] -test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +test = ["flufl.flake8", "importlib-resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] type = ["pytest-mypy"] [[package]] @@ -1364,7 +1378,7 @@ pyyaml = ">=6.0.2,<7.0.0" tomli = {version = ">=1.2.2", markers = "python_version < \"3.11\""} [package.extras] -poetry-plugin = ["poetry (>=1.0,<3.0)"] +poetry-plugin = ["poetry (>=1.0,<3.0) ; python_version < \"4\""] [[package]] name = "pre-commit" @@ -1425,7 +1439,7 @@ typing-extensions = ">=4.12.2" [package.extras] email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] [[package]] name = "pydantic-core" @@ -1830,6 +1844,7 @@ description = "An extremely fast Python linter and code formatter, written in Ru optional = true python-versions = ">=3.7" groups = ["main"] +markers = "extra == \"compiler\"" files = [ {file = "ruff-0.9.1-py3-none-linux_armv6l.whl", hash = "sha256:84330dda7abcc270e6055551aca93fdde1b0685fc4fd358f26410f9349cf1743"}, {file = "ruff-0.9.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3cae39ba5d137054b0e5b472aee3b78a7c884e61591b100aeb544bcd1fc38d4f"}, @@ -1864,13 +1879,13 @@ files = [ ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.8.0)"] -core = ["importlib_metadata (>=6)", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] +core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.14.*)", "pytest-mypy"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] [[package]] name = "six" @@ -2191,7 +2206,7 @@ files = [ ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] @@ -2215,7 +2230,7 @@ platformdirs = ">=3.9.1,<5" [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] [[package]] name = "wcwidth" @@ -2242,18 +2257,19 @@ files = [ ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +test = ["big-O", "importlib-resources ; python_version < \"3.9\"", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] type = ["pytest-mypy"] [extras] compiler = ["jinja2", "ruff"] +grpcio = ["grpcio"] rust-codec = ["betterproto-rust-codec"] [metadata] lock-version = "2.1" python-versions = ">=3.9,<4.0" -content-hash = "cf90b82485ce6837f190477b98778fedc112e9efb6b0dde487da9d65cd92db3b" +content-hash = "3735161eef4bbb7950b783973be15314e826d0fde794132d7913ad3684952fdc" diff --git a/pyproject.toml b/pyproject.toml index 7b1b6e741..d8352a78c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ dynamic = ["dependencies"] # The Ruff version is pinned. To update it, also update it in .pre-commit-config.yaml ruff = { version = "~0.9.1", optional = true } grpclib = "^0.4.1" +grpcio = { version = ">=1.73.0", optional = true } jinja2 = { version = ">=3.0.3", optional = true } python-dateutil = "^2.8" typing-extensions = "^4.7.1" @@ -45,6 +46,7 @@ pydantic = ">=2.0,<3" protobuf = "^5" cachelib = "^0.13.0" tomlkit = ">=0.7.0" +grpcio-testing = "^1.54.2" [project.scripts] protoc-gen-python_betterproto = "betterproto.plugin:main" @@ -52,6 +54,7 @@ protoc-gen-python_betterproto = "betterproto.plugin:main" [project.optional-dependencies] compiler = ["ruff", "jinja2"] rust-codec = ["betterproto-rust-codec"] +grpcio = ["grpcio"] [tool.ruff] extend-exclude = ["tests/output_*"] diff --git a/src/betterproto/grpc/grpcio_client.py b/src/betterproto/grpc/grpcio_client.py new file mode 100644 index 000000000..b3cc316d6 --- /dev/null +++ b/src/betterproto/grpc/grpcio_client.py @@ -0,0 +1,120 @@ +from abc import ABC +from typing import ( + TYPE_CHECKING, + AsyncIterable, + AsyncIterator, + Iterable, + Mapping, + Optional, + Union, +) + +import grpc + +if TYPE_CHECKING: + from .._types import ( + T, + IProtoMessage, + ) + +Value = Union[str, bytes] +MetadataLike = Union[Mapping[str, Value], Iterable[tuple[str, Value]]] +MessageSource = Union[Iterable["IProtoMessage"], AsyncIterable["IProtoMessage"]] + + +class ServiceStub(ABC): + + def __init__( + self, + channel: grpc.aio.Channel, + *, + timeout: Optional[float] = None, + metadata: Optional[MetadataLike] = None, + ) -> None: + self.channel = channel + self.timeout = timeout + self.metadata = metadata + + def _resolve_request_kwargs( + self, + timeout: Optional[float], + metadata: Optional[MetadataLike], + ): + return { + "timeout": self.timeout if timeout is None else timeout, + "metadata": self.metadata if metadata is None else metadata, + } + + async def _unary_unary( + self, + stub_method: grpc.aio.UnaryUnaryMultiCallable, + request: "IProtoMessage", + *, + timeout: Optional[float] = None, + metadata: Optional[MetadataLike] = None, + ) -> "T": + return await stub_method( + request, + **self._resolve_request_kwargs(timeout, metadata), + ) + + async def _unary_stream( + self, + stub_method: grpc.aio.UnaryStreamMultiCallable, + request: "IProtoMessage", + *, + timeout: Optional[float] = None, + metadata: Optional[MetadataLike] = None, + ) -> AsyncIterator["T"]: + call = stub_method( + request, + **self._resolve_request_kwargs(timeout, metadata), + ) + async for response in call: + yield response + + async def _stream_unary( + self, + stub_method: grpc.aio.StreamUnaryMultiCallable, + request_iterator: MessageSource, + *, + timeout: Optional[float] = None, + metadata: Optional[MetadataLike] = None, + ) -> "T": + call = stub_method( + self._wrap_message_iterator(request_iterator), + **self._resolve_request_kwargs(timeout, metadata), + ) + return await call + + async def _stream_stream( + self, + stub_method: grpc.aio.StreamStreamMultiCallable, + request_iterator: MessageSource, + *, + timeout: Optional[float] = None, + metadata: Optional[MetadataLike] = None, + ) -> AsyncIterator["T"]: + call = stub_method( + self._wrap_message_iterator(request_iterator), + **self._resolve_request_kwargs(timeout, metadata), + ) + async for response in call: + yield response + + @staticmethod + def _wrap_message_iterator( + messages: MessageSource, + ) -> AsyncIterator["IProtoMessage"]: + if hasattr(messages, '__aiter__'): + async def async_wrapper(): + async for message in messages: + yield message + + return async_wrapper() + else: + async def sync_wrapper(): + for message in messages: + yield message + + return sync_wrapper() diff --git a/src/betterproto/grpc/grpcio_server.py b/src/betterproto/grpc/grpcio_server.py new file mode 100644 index 000000000..72c4113fe --- /dev/null +++ b/src/betterproto/grpc/grpcio_server.py @@ -0,0 +1,30 @@ +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Dict + + +if TYPE_CHECKING: + import grpc + + +class ServiceBase(ABC): + + @property + @abstractmethod + def __rpc_methods__(self) -> Dict[str, "grpc.RpcMethodHandler"]: ... + + @property + @abstractmethod + def __proto_path__(self) -> str: ... + + +def register_servicers(server: "grpc.aio.Server", *servicers: ServiceBase): + from grpc import method_handlers_generic_handler + + server.add_generic_rpc_handlers( + tuple( + method_handlers_generic_handler( + servicer.__proto_path__, servicer.__rpc_methods__ + ) + for servicer in servicers + ) + ) diff --git a/src/betterproto/plugin/models.py b/src/betterproto/plugin/models.py index e330e6884..9ecb28144 100644 --- a/src/betterproto/plugin/models.py +++ b/src/betterproto/plugin/models.py @@ -270,6 +270,7 @@ class OutputTemplate: imports_type_checking_only: Set[str] = field(default_factory=set) pydantic_dataclasses: bool = False output: bool = True + use_grpcio: bool = False typing_compiler: TypingCompiler = field(default_factory=DirectImportTypingCompiler) @property @@ -697,18 +698,20 @@ class ServiceMethodCompiler(ProtoContentBase): proto_obj: MethodDescriptorProto path: List[int] = PLACEHOLDER comment_indent: int = 8 + use_grpcio: bool = False def __post_init__(self) -> None: # Add method to service self.parent.methods.append(self) - self.output_file.imports_type_checking_only.add("import grpclib.server") - self.output_file.imports_type_checking_only.add( - "from betterproto.grpc.grpclib_client import MetadataLike" - ) - self.output_file.imports_type_checking_only.add( - "from grpclib.metadata import Deadline" - ) + if self.use_grpcio: + imports = ["import grpc.aio", "from betterproto.grpc.grpcio_client import MetadataLike"] + else: + imports = ["import grpclib.server", "from betterproto.grpc.grpclib_client import MetadataLike", + "from grpclib.metadata import Deadline"] + + for import_line in imports: + self.output_file.imports_type_checking_only.add(import_line) super().__post_init__() # check for unset fields diff --git a/src/betterproto/plugin/parser.py b/src/betterproto/plugin/parser.py index 5f7b72c40..e01262354 100644 --- a/src/betterproto/plugin/parser.py +++ b/src/betterproto/plugin/parser.py @@ -40,10 +40,11 @@ from .typing_compiler import ( DirectImportTypingCompiler, NoTyping310TypingCompiler, - TypingCompiler, TypingImportTypingCompiler, ) +USE_GRPCIO_FLAG = "USE_GRPCIO" + def traverse( proto_file: FileDescriptorProto, @@ -80,6 +81,7 @@ def generate_code(request: CodeGeneratorRequest) -> CodeGeneratorResponse: response.supported_features = CodeGeneratorResponseFeature.FEATURE_PROTO3_OPTIONAL request_data = PluginRequestCompiler(plugin_request_obj=request) + use_grpcio = USE_GRPCIO_FLAG in plugin_options # Gather output packages for proto_file in request.proto_file: output_package_name = proto_file.package @@ -90,7 +92,7 @@ def generate_code(request: CodeGeneratorRequest) -> CodeGeneratorResponse: ) # Add this input file to the output corresponding to this package request_data.output_packages[output_package_name].input_files.append(proto_file) - + request_data.output_packages[output_package_name].use_grpcio = use_grpcio if ( proto_file.package == "google.protobuf" and "INCLUDE_GOOGLE" not in plugin_options @@ -143,7 +145,7 @@ def generate_code(request: CodeGeneratorRequest) -> CodeGeneratorResponse: for output_package_name, output_package in request_data.output_packages.items(): for proto_input_file in output_package.input_files: for index, service in enumerate(proto_input_file.service): - read_protobuf_service(proto_input_file, service, index, output_package) + read_protobuf_service(proto_input_file, service, index, output_package, use_grpcio) # Generate output files output_paths: Set[pathlib.Path] = set() @@ -253,6 +255,7 @@ def read_protobuf_service( service: ServiceDescriptorProto, index: int, output_package: OutputTemplate, + use_grpcio: bool = False, ) -> None: service_data = ServiceCompiler( source_file=source_file, @@ -266,4 +269,5 @@ def read_protobuf_service( parent=service_data, proto_obj=method, path=[6, index, 2, j], + use_grpcio=use_grpcio, ) diff --git a/src/betterproto/templates/header.py.j2 b/src/betterproto/templates/header.py.j2 index b6d0a6c44..88460d73b 100644 --- a/src/betterproto/templates/header.py.j2 +++ b/src/betterproto/templates/header.py.j2 @@ -42,8 +42,16 @@ from pydantic import {% for i in output_file.pydantic_imports|sort %}{{ i }}{% i {% endif %} + +{% if output_file.use_grpcio %} +import grpc +from betterproto.grpc.grpcio_client import ServiceStub +from betterproto.grpc.grpcio_server import ServiceBase +{% endif %} + import betterproto -{% if output_file.services %} +{% if not output_file.use_grpcio %} +from betterproto.grpc.grpclib_client import ServiceStub from betterproto.grpc.grpclib_server import ServiceBase import grpclib {% endif %} diff --git a/src/betterproto/templates/template.py.j2 b/src/betterproto/templates/template.py.j2 index 4a252aec6..225190e93 100644 --- a/src/betterproto/templates/template.py.j2 +++ b/src/betterproto/templates/template.py.j2 @@ -64,13 +64,15 @@ class {{ message.py_name }}(betterproto.Message): {% endfor %} {% for service in output_file.services %} +{% if output_file.use_grpcio %} +class {{ service.py_name }}Stub(ServiceStub): +{% else %} class {{ service.py_name }}Stub(betterproto.ServiceStub): +{% endif %} {% if service.comment %} {{ service.comment }} - - {% elif not service.methods %} - pass {% endif %} + {% for method in service.methods %} async def {{ method.py_name }}(self {%- if not method.client_streaming -%} @@ -79,22 +81,73 @@ class {{ service.py_name }}Stub(betterproto.ServiceStub): {# Client streaming: need a request iterator instead #} , {{ method.py_input_message_param }}_iterator: "{{ output_file.typing_compiler.union(output_file.typing_compiler.async_iterable(method.py_input_message_type), output_file.typing_compiler.iterable(method.py_input_message_type)) }}" {%- endif -%} - , - * , timeout: {{ output_file.typing_compiler.optional("float") }} = None + {% if not output_file.use_grpcio %} , deadline: {{ output_file.typing_compiler.optional('"Deadline"') }} = None + {% endif %} , metadata: {{ output_file.typing_compiler.optional('"MetadataLike"') }} = None ) -> "{% if method.server_streaming %}{{ output_file.typing_compiler.async_iterator(method.py_output_message_type ) }}{% else %}{{ method.py_output_message_type }}{% endif %}": {% if method.comment %} {{ method.comment }} {% endif %} - {% if method.proto_obj.options.deprecated %} - warnings.warn("{{ service.py_name }}.{{ method.py_name }} is deprecated", DeprecationWarning) - - {% endif %} - {% if method.server_streaming %} - {% if method.client_streaming %} + {% if output_file.use_grpcio %} + {% if method.server_streaming %} + {% if method.client_streaming %} + # stream_stream pour grpcio - direct call + call = self.channel.stream_stream( + "{{ method.route }}", + request_serializer={{ method.py_input_message_type }}.SerializeToString, + response_deserializer={{ method.py_output_message_type.strip('"') }}.FromString, + )( + self._wrap_message_iterator({{ method.py_input_message_param }}_iterator), + timeout=timeout, + metadata=metadata, + ) + async for response in call: + yield response + {% else %} + # unary_stream pour grpcio - direct call + call = self.channel.unary_stream( + "{{ method.route }}", + request_serializer={{ method.py_input_message_type }}.SerializeToString, + response_deserializer={{ method.py_output_message_type.strip('"') }}.FromString, + )( + {{ method.py_input_message_param }}, + timeout=timeout, + metadata=metadata, + ) + async for response in call: + yield response + {% endif %} + {% else %} + {% if method.client_streaming %} + # stream_unary pour grpcio - direct call + return await self.channel.stream_unary( + "{{ method.route }}", + request_serializer={{ method.py_input_message_type }}.SerializeToString, + response_deserializer={{ method.py_output_message_type.strip('"') }}.FromString, + )( + self._wrap_message_iterator({{ method.py_input_message_param }}_iterator), + timeout=timeout, + metadata=metadata, + ) + {% else %} + # unary_unary pour grpcio - direct call + return await self.channel.unary_unary( + "{{ method.route }}", + request_serializer={{ method.py_input_message_type }}.SerializeToString, + response_deserializer={{ method.py_output_message_type.strip('"') }}.FromString, + )( + {{ method.py_input_message_param }}, + timeout=timeout, + metadata=metadata, + ) + {% endif %} + {% endif %} + {% else %} + {% if method.server_streaming %} + {% if method.client_streaming %} async for response in self._stream_stream( "{{ method.route }}", {{ method.py_input_message_param }}_iterator, @@ -105,7 +158,7 @@ class {{ service.py_name }}Stub(betterproto.ServiceStub): metadata=metadata, ): yield response - {% else %}{# i.e. not client streaming #} + {% else %} async for response in self._unary_stream( "{{ method.route }}", {{ method.py_input_message_param }}, @@ -115,10 +168,9 @@ class {{ service.py_name }}Stub(betterproto.ServiceStub): metadata=metadata, ): yield response - - {% endif %}{# if client streaming #} - {% else %}{# i.e. not server streaming #} - {% if method.client_streaming %} + {% endif %} + {% else %} + {% if method.client_streaming %} return await self._stream_unary( "{{ method.route }}", {{ method.py_input_message_param }}_iterator, @@ -128,7 +180,7 @@ class {{ service.py_name }}Stub(betterproto.ServiceStub): deadline=deadline, metadata=metadata, ) - {% else %}{# i.e. not client streaming #} + {% else %} return await self._unary_unary( "{{ method.route }}", {{ method.py_input_message_param }}, @@ -137,7 +189,8 @@ class {{ service.py_name }}Stub(betterproto.ServiceStub): deadline=deadline, metadata=metadata, ) - {% endif %}{# client streaming #} + {% endif %} + {% endif %} {% endif %} {% endfor %} @@ -147,6 +200,8 @@ class {{ service.py_name }}Stub(betterproto.ServiceStub): {{ i }} {% endfor %} + +{% if not output_file.use_grpcio %} {% for service in output_file.services %} class {{ service.py_name }}Base(ServiceBase): {% if service.comment %} @@ -154,6 +209,11 @@ class {{ service.py_name }}Base(ServiceBase): {% endif %} + @property + def __proto_path__(self): + """Return the proto path for this service.""" + return "{% if output_file.package %}{{ output_file.package }}.{% endif %}{{ service.proto_name }}" + {% for method in service.methods %} async def {{ method.py_name }}(self {%- if not method.client_streaming -%} @@ -215,3 +275,75 @@ class {{ service.py_name }}Base(ServiceBase): } {% endfor %} +{% endif %} +{% if output_file.use_grpcio %} +{% for service in output_file.services %} +class {{ service.py_name }}Base(ServiceBase): + {% if service.comment %} +{{ service.comment }} + + {% endif %} + + {% for method in service.methods %} + async def {{ method.py_name }}(self + {%- if not method.client_streaming -%} + , request: "{{ method.py_input_message_type }}" + {%- else -%} + {# Client streaming: need a request iterator instead #} + , request_iterator: AsyncIterator["{{ method.py_input_message_type }}"] + {%- endif -%} + , context: grpc.aio.ServicerContext + ) -> {% if method.server_streaming %}AsyncGenerator["{{ method.py_output_message_type }}", None]{% else %}"{{ method.py_output_message_type }}"{% endif %}: + {% if method.comment %} +{{ method.comment }} + + {% endif %} + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + {% endfor %} + + + @property + def __rpc_methods__(self): + return { + {% for method in service.methods %} + "{{ method.proto_name }}": + {% if not method.client_streaming and not method.server_streaming %} + grpc.unary_unary_rpc_method_handler( + {% elif method.client_streaming and method.server_streaming %} + grpc.stream_stream_rpc_method_handler( + {% elif method.client_streaming %} + grpc.stream_unary_rpc_method_handler( + {% else %} + grpc.unary_stream_rpc_method_handler( + {% endif %} + self.{{ method.py_name }}, + request_deserializer={{ method.py_input_message_type }}.FromString, + response_serializer={{ method.py_output_message_type.strip('"') }}.SerializeToString, + ), + {% endfor %} + } + +{% endfor %} +{% endif %} + +{% if output_file.use_grpcio %} +{% for service in output_file.services %} + +def add_{{ service.py_name }}Servicer_to_server(servicer, server): + """Add {{ service.py_name }}Servicer to the server.""" + rpc_method_handlers = { + {% for method in service.methods %} + "{{ method.proto_name }}": servicer.__rpc_methods__["{{ method.proto_name }}"], + {% endfor %} + } + generic_handler = grpc.method_handlers_generic_handler( + "{% if output_file.package %}{{ output_file.package }}.{% endif %}{{ service.proto_name }}", + rpc_method_handlers + ) + server.add_generic_rpc_handlers((generic_handler,)) + +{% endfor %} +{% endif %} diff --git a/tests/generate.py b/tests/generate.py index 91bdbb8a2..2165725dc 100755 --- a/tests/generate.py +++ b/tests/generate.py @@ -90,17 +90,26 @@ async def generate_test_case_output( clear_directory(test_case_output_path_reference) clear_directory(test_case_output_path_betterproto) - ( - (ref_out, ref_err, ref_code), - (plg_out, plg_err, plg_code), - (plg_out_pyd, plg_err_pyd, plg_code_pyd), - ) = await asyncio.gather( + tasks = [ protoc(test_case_input_path, test_case_output_path_reference, True), protoc(test_case_input_path, test_case_output_path_betterproto, False), - protoc( - test_case_input_path, test_case_output_path_betterproto_pyd, False, True - ), - ) + protoc(test_case_input_path, test_case_output_path_betterproto_pyd, False, True), + ] + + is_service_test_case = test_case_name == "service" + if is_service_test_case: + test_case_output_path_grpcio = output_path_betterproto.joinpath("service_grpcio") + os.makedirs(test_case_output_path_grpcio, exist_ok=True) + clear_directory(test_case_output_path_grpcio) + tasks.append(protoc(test_case_input_path, test_case_output_path_grpcio, False, False, True)) + + results = await asyncio.gather(*tasks) + + if is_service_test_case: + (ref_out, ref_err, ref_code), (plg_out, plg_err, plg_code), (plg_out_pyd, plg_err_pyd, plg_code_pyd), (grpcio_out, grpcio_err, grpcio_code) = results + else: + (ref_out, ref_err, ref_code), (plg_out, plg_err, plg_code), (plg_out_pyd, plg_err_pyd, plg_code_pyd) = results + grpcio_out, grpcio_err, grpcio_code = b"", b"", 0 if ref_code == 0: print(f"\033[31;1;4mGenerated reference output for {test_case_name!r}\033[0m") @@ -161,7 +170,7 @@ async def generate_test_case_output( sys.stderr.buffer.write(plg_err_pyd) sys.stderr.buffer.flush() - return max(ref_code, plg_code, plg_code_pyd) + return max(ref_code, plg_code, plg_code_pyd, grpcio_code) HELP = "\n".join( diff --git a/tests/grpc/test_grpcio_client.py b/tests/grpc/test_grpcio_client.py new file mode 100644 index 000000000..c28f65104 --- /dev/null +++ b/tests/grpc/test_grpcio_client.py @@ -0,0 +1,226 @@ +import pytest +from unittest.mock import MagicMock + +from tests.output_betterproto.service_grpcio import ( + DoThingRequest, + DoThingResponse, + GetThingRequest, + GetThingResponse, + TestStub as ThingServiceGrpcioClient, +) + +from .thing_service_grpcio import ThingServiceGrpcio, create_test_channel + + +async def _test_grpcio_client(client: ThingServiceGrpcioClient, name="clean room", **kwargs): + response = await client.do_thing(DoThingRequest(name=name), **kwargs) + assert response.names == [name] + + +def _assert_request_meta_received_grpcio(timeout, metadata): + def server_side_test(context): + if hasattr(context, 'timeout') and timeout: + assert context.timeout == pytest.approx(timeout, 1), ( + "The provided timeout should be received serverside" + ) + if hasattr(context, 'metadata') and metadata: + for key, value in metadata.items(): + assert context.metadata.get(key) == value, ( + f"The provided {key} metadata should be received serverside" + ) + return server_side_test + + +@pytest.mark.asyncio +async def test_simple_grpcio_service_call(): + service = ThingServiceGrpcio() + channel = create_test_channel(service) + + client = ThingServiceGrpcioClient(channel=MagicMock()) + + async def mock_unary_unary(method, request, response_type, **kwargs): + context = MagicMock() + context.timeout = kwargs.get('timeout') + context.metadata = kwargs.get('metadata', {}) + return channel.route_call(method, request, context) + + client._unary_unary = mock_unary_unary + await _test_grpcio_client(client) + + +@pytest.mark.asyncio +async def test_grpcio_service_call_mutable_defaults(mocker): + """Test that mutable defaults don't cause issues using ThingServiceGrpcio""" + service = ThingServiceGrpcio() + channel = create_test_channel(service) + client = ThingServiceGrpcioClient(channel=MagicMock()) + + spy = mocker.spy(client, '_unary_unary') + + async def mock_unary_unary(method, request, response_type, **kwargs): + context = MagicMock() + return channel.route_call(method, request, context) + + client._unary_unary = mock_unary_unary + + await _test_grpcio_client(client, name="test1") + await _test_grpcio_client(client, name="test2") + + # Verify calls were made with different request objects + if len(spy.call_args_list) >= 2: + comments1 = spy.call_args_list[0].args[1].comments if len(spy.call_args_list[0].args) > 1 else [] + comments2 = spy.call_args_list[1].args[1].comments if len(spy.call_args_list[1].args) > 1 else [] + assert comments1 is not comments2 + + +@pytest.mark.asyncio +async def test_grpcio_service_call_with_upfront_request_params(): + timeout = 30 + metadata = {"authorization": "12345"} + + def test_hook(context): + assert context.timeout == timeout + assert context.metadata.get("authorization") == "12345" + + service = ThingServiceGrpcio(test_hook=test_hook) + channel = create_test_channel(service) + client = ThingServiceGrpcioClient(channel=MagicMock(), timeout=timeout, metadata=metadata) + + async def mock_unary_unary(method, request, response_type, **kwargs): + final_kwargs = client._resolve_request_kwargs( + kwargs.get('timeout'), + kwargs.get('metadata') + ) + context = MagicMock() + context.timeout = final_kwargs.get('timeout') + context.metadata = final_kwargs.get('metadata', {}) + return channel.route_call(method, request, context) + + client._unary_unary = mock_unary_unary + await _test_grpcio_client(client) + + +@pytest.mark.asyncio +async def test_grpcio_unary_stream_request(): + thing_name = "my milkshakes" + + service = ThingServiceGrpcio() + channel = create_test_channel(service) + client = ThingServiceGrpcioClient(channel=MagicMock()) + + async def mock_unary_stream(method, request, response_type, **kwargs): + context = MagicMock() + for response in channel.route_call(method, request, context): + yield response + + client._unary_stream = mock_unary_stream + + expected_versions = [5, 4, 3, 2, 1] + async for response in client.get_thing_versions(GetThingRequest(name=thing_name)): + assert response.name == thing_name + assert response.version == expected_versions.pop(0) + + +@pytest.mark.asyncio +async def test_grpcio_stream_stream_request(): + some_things = ["cake", "cricket", "coral reef"] + expected_things = some_things + + service = ThingServiceGrpcio() + channel = create_test_channel(service) + client = ThingServiceGrpcioClient(channel=MagicMock()) + + async def mock_stream_stream(method, request_iterator, request_type, response_type, **kwargs): + # Convert async iterator to list + requests = [req async for req in client._wrap_message_iterator(request_iterator)] + context = MagicMock() + for response in channel.route_call(method, requests, context): + yield response + + client._stream_stream = mock_stream_stream + + requests = [GetThingRequest(name) for name in some_things] + response_index = 0 + async for response in client.get_different_things(requests): + assert response.name == expected_things[response_index] + assert response.version == response_index + 1 + response_index += 1 + + assert response_index == len(expected_things), ( + "Didn't receive all expected responses" + ) + + +@pytest.mark.asyncio +async def test_grpcio_stream_unary_with_empty_iterable(): + things = [] + + service = ThingServiceGrpcio() + channel = create_test_channel(service) + client = ThingServiceGrpcioClient(channel=MagicMock()) + + async def mock_stream_unary(method, request_iterator, request_type, response_type, **kwargs): + requests = [req async for req in client._wrap_message_iterator(request_iterator)] + context = MagicMock() + return channel.route_call(method, requests, context) + + client._stream_unary = mock_stream_unary + + requests = [DoThingRequest(name) for name in things] + response = await client.do_many_things(requests) + assert len(response.names) == 0 + + +@pytest.mark.asyncio +async def test_grpcio_stream_stream_with_empty_iterable(): + things = [] + service = ThingServiceGrpcio() + channel = create_test_channel(service) + client = ThingServiceGrpcioClient(channel=MagicMock()) + + async def mock_stream_stream(method, request_iterator, request_type, response_type, **kwargs): + requests = [req async for req in client._wrap_message_iterator(request_iterator)] + context = MagicMock() + for response in channel.route_call(method, requests, context): + yield response + + client._stream_stream = mock_stream_stream + + requests = [GetThingRequest(name) for name in things] + responses = [ + response async for response in client.get_different_things(requests) + ] + assert len(responses) == 0 + + +@pytest.mark.asyncio +async def test_grpcio_messages_validation(): + request = DoThingRequest(name="test message", comments=["comment1", "comment2"]) + assert request.name == "test message" + assert request.comments == ["comment1", "comment2"] + response = DoThingResponse(names=["response1", "response2"]) + assert response.names == ["response1", "response2"] + get_request = GetThingRequest(name="get test") + assert get_request.name == "get test" + get_response = GetThingResponse(name="thing", version=1) + assert get_response.name == "thing" + assert get_response.version == 1 + + +@pytest.mark.asyncio +async def test_grpcio_client_method_signatures(): + client = ThingServiceGrpcioClient(channel=MagicMock()) + assert hasattr(client, "do_thing") + assert hasattr(client, "do_many_things") + assert hasattr(client, "get_thing_versions") + assert hasattr(client, "get_different_things") + + import inspect + + do_thing_sig = inspect.signature(client.do_thing) + assert "do_thing_request" in do_thing_sig.parameters + assert "timeout" in do_thing_sig.parameters + assert "metadata" in do_thing_sig.parameters + + do_many_sig = inspect.signature(client.do_many_things) + assert "do_thing_request_iterator" in do_many_sig.parameters diff --git a/tests/grpc/thing_service_grpcio.py b/tests/grpc/thing_service_grpcio.py new file mode 100644 index 000000000..f31315c40 --- /dev/null +++ b/tests/grpc/thing_service_grpcio.py @@ -0,0 +1,60 @@ +from typing import Iterator + +from tests.output_betterproto.service_grpcio import ( + DoThingRequest, + DoThingResponse, + GetThingRequest, + GetThingResponse, +) + + +class ThingServiceGrpcio: + + def __init__(self, test_hook=None): + self.test_hook = test_hook + + def do_thing(self, request: DoThingRequest, context=None) -> DoThingResponse: + if self.test_hook is not None: + self.test_hook(context) + return DoThingResponse(names=[request.name]) + + def do_many_things(self, request_iterator: Iterator[DoThingRequest], context=None) -> DoThingResponse: + thing_names = [request.name for request in request_iterator] + if self.test_hook is not None: + self.test_hook(context) + return DoThingResponse(names=thing_names) + + def get_thing_versions(self, request: GetThingRequest, context=None) -> Iterator[GetThingResponse]: + if self.test_hook is not None: + self.test_hook(context) + for version_num in range(5, 0, -1): # 5, 4, 3, 2, 1 + yield GetThingResponse(name=request.name, version=version_num) + + def get_different_things(self, request_iterator: Iterator[GetThingRequest], context=None) -> Iterator[GetThingResponse]: + if self.test_hook is not None: + self.test_hook(context) + version = 1 + for request in request_iterator: + yield GetThingResponse(name=request.name, version=version) + version += 1 + + +class GrpcioTestChannel: + def __init__(self, service: ThingServiceGrpcio): + self.service = service + + def route_call(self, method: str, request, context=None): + if method == "/service.Test/DoThing": + return self.service.do_thing(request, context) + elif method == "/service.Test/DoManyThings": + return self.service.do_many_things(request, context) + elif method == "/service.Test/GetThingVersions": + return self.service.get_thing_versions(request, context) + elif method == "/service.Test/GetDifferentThings": + return self.service.get_different_things(request, context) + else: + raise NotImplementedError(f"Method {method} not implemented") + + +def create_test_channel(service_instance: ThingServiceGrpcio): + return GrpcioTestChannel(service_instance) diff --git a/tests/util.py b/tests/util.py index 22c4f9012..f7255ff79 100644 --- a/tests/util.py +++ b/tests/util.py @@ -18,6 +18,7 @@ Union, ) +from betterproto.plugin.parser import USE_GRPCIO_FLAG os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"] = "python" @@ -44,12 +45,13 @@ async def protoc( output_dir: Union[str, Path], reference: bool = False, pydantic_dataclasses: bool = False, + grpcio: bool = False, ): path: Path = Path(path).resolve() output_dir: Path = Path(output_dir).resolve() python_out_option: str = "python_betterproto_out" if not reference else "python_out" - if pydantic_dataclasses: + if pydantic_dataclasses or grpcio: plugin_path = Path("src/betterproto/plugin/main.py") if "Win" in platform.system(): @@ -76,11 +78,20 @@ async def protoc( "grpc.tools.protoc", f"--plugin=protoc-gen-custom={plugin_path.as_posix()}", "--experimental_allow_proto3_optional", - "--custom_opt=pydantic_dataclasses", - f"--proto_path={path.as_posix()}", - f"--custom_out={output_dir.as_posix()}", - *[p.as_posix() for p in path.glob("*.proto")], ] + + if pydantic_dataclasses: + command.append("--custom_opt=pydantic_dataclasses") + if grpcio: + command.append(f"--custom_opt={USE_GRPCIO_FLAG}") + + command.extend( + [ + f"--proto_path={path.as_posix()}", + f"--custom_out={output_dir.as_posix()}", + *[p.as_posix() for p in path.glob("*.proto")], + ] + ) else: command = [ sys.executable,