Skip to content

Commit

Permalink
Add grpcio support to parse_grpc_uri() (#54)
Browse files Browse the repository at this point in the history
- **Add more gRPC hacks to deal with channels**
- **Make `parse_grpc_uri()` compatible with `grpcio`**
  • Loading branch information
llucax authored Jun 3, 2024
2 parents de8ec69 + 08bba2a commit ae11e6e
Show file tree
Hide file tree
Showing 4 changed files with 251 additions and 71 deletions.
3 changes: 2 additions & 1 deletion RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@

## Upgrading

<!-- Here goes notes on how to upgrade from previous versions, including deprecations and what they should be replaced with -->
- `channel.parse_grpc_uri()` takes an extra argument, the channel type (which can be either `grpclib.client.Channel` or `grpcio.aio.Channel`).

## New Features

- Add a `exception` module to provide client exceptions, including gRPC errors with one subclass per gRPC error status code.
- `channel.parse_grpc_uri()` can now be used with `grpcio` too.

## Bug Fixes

Expand Down
127 changes: 119 additions & 8 deletions src/frequenz/client/base/_grpchacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,46 @@

"""Hacks to deal with multiple grpc libraries.
This module conditionally imports the base exceptions from the `grpclib` and `grpcio`
libraries, assigning them a new name:
This module conditionally imports symbols from the `grpclib` and `grpcio` libraries,
assigning them a new name.
- [`GrpclibError`][] for [`grpclib.GRPCError`][]
- [`GrpcioError`][] for [`grpc.aio.AioRpcError`][]
for `grpclib`:
If the libraries are not installed, the module defines dummy classes with the same names
- `GrpclibError` for `grpclib.GRPCError`
- `GrpclibChannel` for `grpclib.client.Channel`
For `grpcio`:
- `GrpcioError` for `grpc.aio.AioRpcError`
- `GrpcioChannel` for `grpc.aio.Channel`
- `GrpcioSslChannelCredentials` for `grpc.ssl_channel_credentials`
- `grpcio_insecure_channel` for `grpc.aio.insecure_channel`
- `grpcio_secure_channel` for `grpc.aio.secure_channel`
If the libraries are not installed, the module defines dummy symbols with the same names
to avoid import errors.
This way exceptions can be caught from both libraries independently of which one is
used. The unused library will just never raise any exceptions.
This way exceptions code can be written to work with both libraries assuming both are
aviailable, and the correct symbols will be imported at runtime.
"""


from typing import Any, Self

__all__ = [
"GrpcioChannel",
"GrpcioChannelCredentials",
"GrpcioError",
"GrpclibChannel",
"GrpclibError",
"grpcio_insecure_channel",
"grpcio_secure_channel",
"grpcio_ssl_channel_credentials",
]

try:
from grpclib import GRPCError as GrpclibError
from grpclib.client import Channel as GrpclibChannel
except ImportError:

class GrpclibError(Exception): # type: ignore[no-redef]
Expand All @@ -29,11 +53,57 @@ class GrpclibError(Exception): # type: ignore[no-redef]
this class will never be instantiated.
"""

class GrpclibChannel: # type: ignore[no-redef]
"""A dummy class to avoid import errors.
This class will never be actually used, as it is only used for catching
exceptions from the grpclib library. If the grpclib library is not installed,
this class will never be instantiated.
"""

def __init__(self, target: str):
"""Create an instance."""

async def __aenter__(self) -> Self:
"""Enter a context manager."""
return self

async def __aexit__(
self,
_exc_type: type[BaseException] | None,
_exc_val: BaseException | None,
_exc_tb: Any | None,
) -> bool | None:
"""Exit a context manager."""
return None


try:
from grpc import ChannelCredentials as GrpcioChannelCredentials
from grpc import ssl_channel_credentials as grpcio_ssl_channel_credentials
from grpc.aio import AioRpcError as GrpcioError
from grpc.aio import Channel as GrpcioChannel
from grpc.aio import insecure_channel as grpcio_insecure_channel
from grpc.aio import secure_channel as grpcio_secure_channel
except ImportError:

class GrpcioChannelCredentials: # type: ignore[no-redef]
"""A dummy class to avoid import errors.
This class will never be actually used, as it is only used for catching
exceptions from the grpc library. If the grpc library is not installed,
this class will never be instantiated.
"""

def grpcio_ssl_channel_credentials() -> GrpcioChannelCredentials: # type: ignore[misc]
"""Create a dummy function to avoid import errors.
This function will never be actually used, as it is only used for catching
exceptions from the grpc library. If the grpc library is not installed,
this function will never be called.
"""
return GrpcioChannelCredentials()

class GrpcioError(Exception): # type: ignore[no-redef]
"""A dummy class to avoid import errors.
Expand All @@ -42,5 +112,46 @@ class GrpcioError(Exception): # type: ignore[no-redef]
this class will never be instantiated.
"""

class GrpcioChannel: # type: ignore[no-redef]
"""A dummy class to avoid import errors.
This class will never be actually used, as it is only used for catching
exceptions from the grpc library. If the grpc library is not installed,
this class will never be instantiated.
"""

async def __aenter__(self) -> Self:
"""Enter a context manager."""
return self

async def __aexit__(
self,
_exc_type: type[BaseException] | None,
_exc_val: BaseException | None,
_exc_tb: Any | None,
) -> bool | None:
"""Exit a context manager."""
return None

def grpcio_insecure_channel( # type: ignore[misc]
target: str, # pylint: disable=unused-argument
) -> GrpcioChannel:
"""Create a dummy function to avoid import errors.
This function will never be actually used, as it is only used for catching
exceptions from the grpc library. If the grpc library is not installed,
this function will never be called.
"""
return GrpcioChannel()

def grpcio_secure_channel( # type: ignore[misc]
target: str, # pylint: disable=unused-argument
credentials: GrpcioChannelCredentials, # pylint: disable=unused-argument
) -> GrpcioChannel:
"""Create a dummy function to avoid import errors.
__all__ = ["GrpclibError", "GrpcioError"]
This function will never be actually used, as it is only used for catching
exceptions from the grpc library. If the grpc library is not installed,
this function will never be called.
"""
return GrpcioChannel()
33 changes: 26 additions & 7 deletions src/frequenz/client/base/channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@

"""Handling of gRPC channels."""

from typing import TypeVar
from urllib.parse import parse_qs, urlparse

from grpclib.client import Channel
from . import _grpchacks


def _to_bool(value: str) -> bool:
Expand All @@ -17,7 +18,13 @@ def _to_bool(value: str) -> bool:
raise ValueError(f"Invalid boolean value '{value}'")


def parse_grpc_uri(uri: str, /, *, default_port: int = 9090) -> Channel:
ChannelT = TypeVar("ChannelT", _grpchacks.GrpclibChannel, _grpchacks.GrpcioChannel)
"""A `grpclib` or `grpcio` channel type."""


def parse_grpc_uri(
uri: str, channel_type: type[ChannelT], /, *, default_port: int = 9090
) -> ChannelT:
"""Create a grpclib client channel from a URI.
The URI must have the following format:
Expand All @@ -38,6 +45,7 @@ def parse_grpc_uri(uri: str, /, *, default_port: int = 9090) -> Channel:
Args:
uri: The gRPC URI specifying the connection parameters.
channel_type: The type of channel to create.
default_port: The default port number to use if the URI does not specify one.
Returns:
Expand Down Expand Up @@ -68,8 +76,19 @@ def parse_grpc_uri(uri: str, /, *, default_port: int = 9090) -> Channel:
uri,
)

return Channel(
host=parsed_uri.hostname,
port=parsed_uri.port or default_port,
ssl=ssl,
)
host = parsed_uri.hostname
port = parsed_uri.port or default_port
match channel_type:
case _grpchacks.GrpcioChannel:
target = f"{host}:{port}"
return (
_grpchacks.grpcio_secure_channel(
target, _grpchacks.grpcio_ssl_channel_credentials()
)
if ssl
else _grpchacks.grpcio_insecure_channel(target)
)
case _grpchacks.GrpclibChannel:
return _grpchacks.GrpclibChannel(host=host, port=port, ssl=ssl)
case _:
assert False, "Unexpected channel type: {channel_type}"
Loading

0 comments on commit ae11e6e

Please sign in to comment.