"""Client connection options for the async Python wrapper.""" from __future__ import annotations from dataclasses import dataclass from pathlib import Path import grpc from .auth import REDACTED, ApiKey @dataclass(frozen=True) class ClientOptions: """Connection settings for `GatewayClient.connect`.""" endpoint: str api_key: str | ApiKey | None = None plaintext: bool = False ca_file: str | None = None server_name_override: str | None = None call_timeout: float | None = 30.0 stream_timeout: float | None = None max_grpc_message_bytes: int = 16 * 1024 * 1024 def __post_init__(self) -> None: """Validate options; raise `ValueError` for invalid combinations.""" if not self.endpoint: raise ValueError("endpoint must not be empty") if self.plaintext and self.ca_file: raise ValueError("ca_file cannot be used with plaintext connections") if self.call_timeout is not None and self.call_timeout <= 0: raise ValueError("call_timeout must be greater than zero") if self.stream_timeout is not None and self.stream_timeout <= 0: raise ValueError("stream_timeout must be greater than zero") if self.max_grpc_message_bytes <= 0: raise ValueError("max_grpc_message_bytes must be greater than zero") def __repr__(self) -> str: """Return a repr that redacts the API key value.""" api_key = REDACTED if self.api_key else None return ( f"{type(self).__name__}(endpoint={self.endpoint!r}, " f"api_key={api_key!r}, plaintext={self.plaintext!r}, " f"ca_file={self.ca_file!r}, " f"server_name_override={self.server_name_override!r}, " f"call_timeout={self.call_timeout!r}, " f"stream_timeout={self.stream_timeout!r}, " f"max_grpc_message_bytes={self.max_grpc_message_bytes!r})" ) def create_channel(options: ClientOptions) -> grpc.aio.Channel: """Create a plaintext or TLS `grpc.aio` channel from client options.""" channel_options: list[tuple[str, str | int]] = [ ("grpc.max_receive_message_length", options.max_grpc_message_bytes), ("grpc.max_send_message_length", options.max_grpc_message_bytes), ] if options.server_name_override: channel_options.append(("grpc.ssl_target_name_override", options.server_name_override)) if options.plaintext: return grpc.aio.insecure_channel(options.endpoint, options=channel_options) root_certificates = None if options.ca_file: root_certificates = Path(options.ca_file).read_bytes() credentials = grpc.ssl_channel_credentials(root_certificates=root_certificates) return grpc.aio.secure_channel( options.endpoint, credentials, options=channel_options, )